diff options
Diffstat (limited to 'toolkit/components/nimbus/test/unit/test_ExperimentManager_prefs.js')
-rw-r--r-- | toolkit/components/nimbus/test/unit/test_ExperimentManager_prefs.js | 3152 |
1 files changed, 3152 insertions, 0 deletions
diff --git a/toolkit/components/nimbus/test/unit/test_ExperimentManager_prefs.js b/toolkit/components/nimbus/test/unit/test_ExperimentManager_prefs.js new file mode 100644 index 0000000000..7ae802fbb7 --- /dev/null +++ b/toolkit/components/nimbus/test/unit/test_ExperimentManager_prefs.js @@ -0,0 +1,3152 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { + _ExperimentFeature: ExperimentFeature, + NimbusFeatures, +} = ChromeUtils.import("resource://nimbus/ExperimentAPI.jsm"); + +const { PrefUtils } = ChromeUtils.import( + "resource://normandy/lib/PrefUtils.jsm" +); + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); +const { TelemetryEvents } = ChromeUtils.import( + "resource://normandy/lib/TelemetryEvents.jsm" +); + +/** + * Pick a single entry from an object and return a new object containing only + * that entry. + * + * @param {object} obj The object to copy the value from. + * @param {string} key The key whose value is to be copied. + * + * @returns {object} An object with the property `key` set to `obj[key]`. + */ +function pick(obj, key) { + return { [key]: obj[key] }; +} + +const PREF_FEATURES = [ + new ExperimentFeature("test-set-pref", { + description: "Test feature that sets a pref on the default branch.", + owner: "test@test.test", + hasExposure: false, + isEarlyStartup: false, + variables: { + foo: { + type: "string", + description: "Test variable", + setPref: "nimbus.test-only.foo", + }, + }, + }), + new ExperimentFeature("test-set-user-pref", { + description: "Test feature that sets a pref on the user branch.", + owner: "test@test.test", + hasExposure: false, + isEarlyStartup: true, + variables: { + bar: { + type: "string", + description: "Test variable", + setPref: "nimbus.test-only.bar", + }, + }, + }), +]; + +const DEFAULT_VALUE = "default-value"; +const USER_VALUE = "user-value"; +const EXPERIMENT_VALUE = "experiment-value"; +const ROLLOUT_VALUE = "rollout-value"; +const OVERWRITE_VALUE = "overwrite-value"; + +const USER = "user"; +const DEFAULT = "default"; +const ROLLOUT = "rollout"; +const EXPERIMENT = "experiment"; + +const PREFS = { + [DEFAULT]: "nimbus.test-only.foo", + [USER]: "nimbus.test-only.bar", +}; + +const FEATURE_IDS = { + [DEFAULT]: "test-set-pref", + [USER]: "test-set-user-pref", +}; + +const CONFIGS = { + [DEFAULT]: { + [ROLLOUT]: { + featureId: FEATURE_IDS[DEFAULT], + value: { + foo: ROLLOUT_VALUE, + }, + }, + [EXPERIMENT]: { + featureId: FEATURE_IDS[DEFAULT], + value: { + foo: EXPERIMENT_VALUE, + }, + }, + }, + [USER]: { + [ROLLOUT]: { + featureId: FEATURE_IDS[USER], + value: { + bar: ROLLOUT_VALUE, + }, + }, + [EXPERIMENT]: { + featureId: FEATURE_IDS[USER], + value: { + bar: EXPERIMENT_VALUE, + }, + }, + }, +}; + +/** + * Set the given pref values on their respective branches (if they are not + * null). + */ +function setPrefs(pref, { defaultBranchValue = null, userBranchValue = null }) { + if (defaultBranchValue !== null) { + Services.prefs + .getDefaultBranch(null) + .setStringPref(pref, defaultBranchValue); + } + + if (userBranchValue !== null) { + Services.prefs.setStringPref(pref, userBranchValue); + } +} + +function assertExpectedPrefValues(pref, branch, expected, visible, msg) { + info(`Assert pref ${pref} on branch ${branch} matches ${expected} ${msg}`); + const hasBranchValue = expected !== null; + const hasVisibleValue = visible !== null; + + function hasValueMsg(hasValue) { + return `Expected pref "${pref}" on the ${branch} branch to${ + hasValue ? " " : " not " + }have a value ${msg}`; + } + + let prefBranch; + switch (branch) { + case USER: + Assert.equal( + Services.prefs.prefHasUserValue(pref), + hasBranchValue, + hasValueMsg(hasBranchValue) + ); + prefBranch = Services.prefs; + break; + + case DEFAULT: + Assert.equal( + Services.prefs.prefHasDefaultValue(pref), + hasBranchValue, + hasValueMsg(hasBranchValue) + ); + prefBranch = Services.prefs.getDefaultBranch(null); + break; + + default: + Assert.ok(false, "invalid pref branch"); + } + + if (hasBranchValue) { + Assert.equal( + prefBranch.getStringPref(pref), + expected, + `Expected pref "${pref} on the ${branch} branch to be ${JSON.stringify( + expected + )} ${msg}` + ); + } + + if (hasVisibleValue) { + Assert.equal( + Services.prefs.getStringPref(pref), + visible, + `Expected pref "${pref}" to be ${JSON.stringify(visible)} ${msg}` + ); + } else { + Assert.ok( + !Services.prefs.prefHasUserValue(pref) && + !Services.prefs.prefHasDefaultValue(pref), + `Expected pref "${pref} to not be set ${msg}` + ); + } +} + +/** + * Assert the store has no active experiments or rollouts. + */ +function assertEmptyStore(store) { + Assert.deepEqual( + store.getAllActive(), + [], + "There should be no experiments active." + ); + + Assert.deepEqual( + store.getAllRollouts(), + [], + "There should be no rollouts active" + ); +} + +/** + * Remove the experiment store. + */ +async function cleanupStore(store) { + // We need to call finalize first to ensure that any pending saves from + // JSONFile.saveSoon overwrite files on disk. + await store._store.finalize(); + await IOUtils.remove(store._store.path); +} + +/** + * Assert the manager has no active pref observers. + */ +function assertNoObservers(manager) { + Assert.equal( + manager._prefs.size, + 0, + "There should be no active pref observers" + ); + Assert.equal( + manager._prefsBySlug.size, + 0, + "There should be no active pref observers" + ); +} + +/** + * Remove all pref observers on the given ExperimentManager. + */ +function removePrefObservers(manager) { + for (const [name, entry] of manager._prefs.entries()) { + Services.prefs.removeObserver(name, entry.observer); + } + + manager._prefs.clear(); + manager._prefsBySlug.clear(); +} + +add_setup(function setup() { + do_get_profile(); + Services.fog.initializeFOG(); + + const cleanupFeatures = ExperimentTestUtils.addTestFeatures(...PREF_FEATURES); + registerCleanupFunction(cleanupFeatures); +}); + +add_task(async function test_enroll_setPref_rolloutsAndExperiments() { + const store = ExperimentFakes.store(); + const manager = ExperimentFakes.manager(store); + + await manager.onStartup(); + + assertEmptyStore(store); + + /** + * Test that prefs are set correctly before and after enrollment and + * unenrollment. + * + * @param {object} options + * @param {string} options.pref + * The name of the pref. + * + * @param {string} options.branch + * The name of the pref branch ("user" or "default"). + * + * @param {object} options.configs + * The rollout and experiment feature configurations. + * + * @param {string?} options.defaultBranchValue + * An optional value to set for the pref on the default branch + * before the first enrollment. + * + * @param {string?} options.userBranchValue + * An optional value to set for the pref on the user branch + * before the first enrollment. + * + * @param {string[]} options.enrollOrder + * The order to do the enrollments. Must only contain + * "experiment" and "rollout" as values. + * + * @param {string[]} options.unenrollOrder + * The order to undo the enrollments. Must only contain + * "experiment" and "rollout" as values. + * + * @param {(string|null)[]} options.expectedValues + * The expected values of the preft on the given branch at each point: + * + * * before enrollment; + * * one entry each each after enrolling in `options.enrollOrder[i]`; and + * * one entry each each after unenrolling in `options.unenrollOrder[i]`. + * + * A value of null indicates that the pref should not be set on that + * branch. + * + * @param {(string|null)[]?} options.visibleValues + * The expected values returned by + * `Services.prefs.getStringPref` (i.e., the user branch if set, + * falling back to the default branch if not), in the same + * order as |options.expectedValues|. + * + * If undefined, then it will default `options.expectedValues`. + */ + async function doBaseTest({ + pref, + branch, + configs, + userBranchValue = undefined, + defaultBranchValue = undefined, + enrollOrder, + unenrollOrder, + expectedValues, + visibleValues = undefined, + }) { + if (visibleValues === undefined) { + visibleValues = expectedValues; + } + + const cleanupFns = {}; + let i = 0; + + setPrefs(pref, { defaultBranchValue, userBranchValue }); + + assertExpectedPrefValues( + pref, + branch, + expectedValues[i], + visibleValues[i], + "before enrollment" + ); + i++; + + for (const enrollmentKind of enrollOrder) { + const isRollout = enrollmentKind === ROLLOUT; + cleanupFns[ + enrollmentKind + ] = await ExperimentFakes.enrollWithFeatureConfig( + configs[enrollmentKind], + { manager, isRollout } + ); + + assertExpectedPrefValues( + pref, + branch, + expectedValues[i], + visibleValues[i], + `after ${enrollmentKind} enrollment` + ); + i++; + } + + for (const enrollmentKind of unenrollOrder) { + await cleanupFns[enrollmentKind](); + + assertExpectedPrefValues( + pref, + branch, + expectedValues[i], + visibleValues[i], + `after ${enrollmentKind} unenrollment` + ); + i++; + } + + assertEmptyStore(store); + Services.prefs.deleteBranch(pref); + } + + // Tests for a feature that would set a pref on the default branch, but the variable is omitted. + { + const branch = DEFAULT; + const pref = PREFS[branch]; + + const configs = { + [ROLLOUT]: { + featureId: FEATURE_IDS[DEFAULT], + value: {}, + }, + [EXPERIMENT]: { + featureId: FEATURE_IDS[DEFAULT], + value: {}, + }, + }; + + const doTest = args => doBaseTest({ pref, branch, ...args }); + + // Enroll in a rollout then unenroll. + await doTest({ + configs: pick(configs, ROLLOUT), + enrollOrder: [ROLLOUT], + unenrollOrder: [ROLLOUT], + expectedValues: [null, null, null], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + configs: pick(configs, ROLLOUT), + enrollOrder: [ROLLOUT], + unenrollOrder: [ROLLOUT], + expectedValues: [DEFAULT_VALUE, DEFAULT_VALUE, DEFAULT_VALUE], + }); + + await doTest({ + userBranchValue: USER_VALUE, + configs: pick(configs, ROLLOUT), + enrollOrder: [ROLLOUT], + unenrollOrder: [ROLLOUT], + expectedValues: [null, null, null], + visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + configs: pick(configs, ROLLOUT), + enrollOrder: [ROLLOUT], + unenrollOrder: [ROLLOUT], + expectedValues: [DEFAULT_VALUE, DEFAULT_VALUE, DEFAULT_VALUE], + visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE], + }); + + // Enroll in an experiment then unenroll. + await doTest({ + configs: pick(configs, EXPERIMENT), + enrollOrder: [EXPERIMENT], + unenrollOrder: [EXPERIMENT], + expectedValues: [null, null, null], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + configs: pick(configs, EXPERIMENT), + enrollOrder: [EXPERIMENT], + unenrollOrder: [EXPERIMENT], + expectedValues: [DEFAULT_VALUE, DEFAULT_VALUE, DEFAULT_VALUE], + }); + + await doTest({ + userBranchValue: USER_VALUE, + configs: pick(configs, EXPERIMENT), + enrollOrder: [EXPERIMENT], + unenrollOrder: [EXPERIMENT], + expectedValues: [null, null, null], + visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + configs: pick(configs, EXPERIMENT), + enrollOrder: [EXPERIMENT], + unenrollOrder: [EXPERIMENT], + expectedValues: [DEFAULT_VALUE, DEFAULT_VALUE, DEFAULT_VALUE], + visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE], + }); + } + + // Test for a feature that would set a pref on the user branch, but the variable is omitted. + { + const branch = USER; + const pref = PREFS[branch]; + + const configs = { + [ROLLOUT]: { + featureId: FEATURE_IDS[DEFAULT], + value: {}, + }, + [EXPERIMENT]: { + featureId: FEATURE_IDS[DEFAULT], + value: {}, + }, + }; + + const doTest = args => doBaseTest({ pref, branch, ...args }); + + // Enroll in a rollout then unenroll. + await doTest({ + configs: pick(configs, ROLLOUT), + enrollOrder: [ROLLOUT], + unenrollOrder: [ROLLOUT], + expectedValues: [null, null, null], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + configs: pick(configs, ROLLOUT), + enrollOrder: [ROLLOUT], + unenrollOrder: [ROLLOUT], + expectedValues: [null, null, null], + visibleValues: [DEFAULT_VALUE, DEFAULT_VALUE, DEFAULT_VALUE], + }); + + await doTest({ + userBranchValue: USER_VALUE, + configs: pick(configs, ROLLOUT), + enrollOrder: [ROLLOUT], + unenrollOrder: [ROLLOUT], + expectedValues: [USER_VALUE, USER_VALUE, USER_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + configs: pick(configs, ROLLOUT), + enrollOrder: [ROLLOUT], + unenrollOrder: [ROLLOUT], + expectedValues: [USER_VALUE, USER_VALUE, USER_VALUE], + }); + + // Enroll in an experiment then unenroll. + await doTest({ + configs: pick(configs, EXPERIMENT), + enrollOrder: [EXPERIMENT], + unenrollOrder: [EXPERIMENT], + expectedValues: [null, null, null], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + configs: pick(configs, EXPERIMENT), + enrollOrder: [EXPERIMENT], + unenrollOrder: [EXPERIMENT], + expectedValues: [null, null, null], + visibleValues: [DEFAULT_VALUE, DEFAULT_VALUE, DEFAULT_VALUE], + }); + + await doTest({ + userBranchValue: USER_VALUE, + configs: pick(configs, EXPERIMENT), + enrollOrder: [EXPERIMENT], + unenrollOrder: [EXPERIMENT], + expectedValues: [USER_VALUE, USER_VALUE, USER_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + configs: pick(configs, EXPERIMENT), + enrollOrder: [EXPERIMENT], + unenrollOrder: [EXPERIMENT], + expectedValues: [USER_VALUE, USER_VALUE, USER_VALUE], + }); + } + + // Tests for a feature that sets prefs on the default branch. + { + const branch = DEFAULT; + const pref = PREFS[branch]; + const configs = CONFIGS[branch]; + + const doTest = args => doBaseTest({ pref, branch, ...args }); + + // Enroll in rollout then unenroll. + await doTest({ + configs: pick(configs, ROLLOUT), + enrollOrder: [ROLLOUT], + unenrollOrder: [ROLLOUT], + expectedValues: [null, ROLLOUT_VALUE, ROLLOUT_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + configs: pick(configs, ROLLOUT), + enrollOrder: [ROLLOUT], + unenrollOrder: [ROLLOUT], + expectedValues: [DEFAULT_VALUE, ROLLOUT_VALUE, DEFAULT_VALUE], + }); + + await doTest({ + userBranchValue: USER_VALUE, + configs: pick(configs, ROLLOUT), + enrollOrder: [ROLLOUT], + unenrollOrder: [ROLLOUT], + expectedValues: [null, ROLLOUT_VALUE, ROLLOUT_VALUE], + visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + configs: pick(configs, ROLLOUT), + enrollOrder: [ROLLOUT], + unenrollOrder: [ROLLOUT], + expectedValues: [DEFAULT_VALUE, ROLLOUT_VALUE, DEFAULT_VALUE], + visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE], + }); + + // Enroll in experiment then unenroll. + await doTest({ + configs: pick(configs, EXPERIMENT), + enrollOrder: [EXPERIMENT], + unenrollOrder: [EXPERIMENT], + expectedValues: [null, EXPERIMENT_VALUE, EXPERIMENT_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + configs: pick(configs, EXPERIMENT), + enrollOrder: [EXPERIMENT], + unenrollOrder: [EXPERIMENT], + expectedValues: [DEFAULT_VALUE, EXPERIMENT_VALUE, DEFAULT_VALUE], + }); + + await doTest({ + userBranchValue: USER_VALUE, + configs: pick(configs, EXPERIMENT), + enrollOrder: [EXPERIMENT], + unenrollOrder: [EXPERIMENT], + expectedValues: [null, EXPERIMENT_VALUE, EXPERIMENT_VALUE], + visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + configs: pick(configs, EXPERIMENT), + enrollOrder: [EXPERIMENT], + unenrollOrder: [EXPERIMENT], + expectedValues: [DEFAULT_VALUE, EXPERIMENT_VALUE, DEFAULT_VALUE], + visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE], + }); + + // Enroll in rollout then experiment; unenroll in reverse order. + await doTest({ + configs, + enrollOrder: [ROLLOUT, EXPERIMENT], + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [ + null, + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + ROLLOUT_VALUE, + ], + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + enrollOrder: [ROLLOUT, EXPERIMENT], + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [ + DEFAULT_VALUE, + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + DEFAULT_VALUE, + ], + }); + + await doTest({ + configs, + userBranchValue: USER_VALUE, + enrollOrder: [ROLLOUT, EXPERIMENT], + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [ + null, + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + ROLLOUT_VALUE, + ], + visibleValues: [ + USER_VALUE, + USER_VALUE, + USER_VALUE, + USER_VALUE, + USER_VALUE, + ], + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + enrollOrder: [ROLLOUT, EXPERIMENT], + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [ + DEFAULT_VALUE, + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + DEFAULT_VALUE, + ], + visibleValues: [ + USER_VALUE, + USER_VALUE, + USER_VALUE, + USER_VALUE, + USER_VALUE, + ], + }); + + // Enroll in rollout then experiment; unenroll in same order. + await doTest({ + configs, + enrollOrder: [ROLLOUT, EXPERIMENT], + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [ + null, + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + ], + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + enrollOrder: [ROLLOUT, EXPERIMENT], + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [ + DEFAULT_VALUE, + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + DEFAULT_VALUE, + ], + }); + + await doTest({ + configs, + userBranchValue: USER_VALUE, + enrollOrder: [ROLLOUT, EXPERIMENT], + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [ + null, + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + ], + visibleValues: [ + USER_VALUE, + USER_VALUE, + USER_VALUE, + USER_VALUE, + USER_VALUE, + ], + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + enrollOrder: [ROLLOUT, EXPERIMENT], + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [ + DEFAULT_VALUE, + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + DEFAULT_VALUE, + ], + visibleValues: [ + USER_VALUE, + USER_VALUE, + USER_VALUE, + USER_VALUE, + USER_VALUE, + ], + }); + + // Enroll in experiment then rollout; unenroll in reverse order. + await doTest({ + configs, + enrollOrder: [EXPERIMENT, ROLLOUT], + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [ + null, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + ], + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + enrollOrder: [EXPERIMENT, ROLLOUT], + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [ + DEFAULT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + DEFAULT_VALUE, + ], + }); + + await doTest({ + configs, + userBranchValue: USER_VALUE, + enrollOrder: [EXPERIMENT, ROLLOUT], + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [ + null, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + ], + visibleValues: [ + USER_VALUE, + USER_VALUE, + USER_VALUE, + USER_VALUE, + USER_VALUE, + ], + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + enrollOrder: [EXPERIMENT, ROLLOUT], + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [ + DEFAULT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + DEFAULT_VALUE, + ], + visibleValues: [ + USER_VALUE, + USER_VALUE, + USER_VALUE, + USER_VALUE, + USER_VALUE, + ], + }); + + // Enroll in experiment then rollout; unenroll in same order. + await doTest({ + configs, + enrollOrder: [EXPERIMENT, ROLLOUT], + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [ + null, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + ROLLOUT_VALUE, + ], + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + enrollOrder: [EXPERIMENT, ROLLOUT], + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [ + DEFAULT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + DEFAULT_VALUE, + ], + }); + + await doTest({ + configs, + userBranchValue: USER_VALUE, + enrollOrder: [EXPERIMENT, ROLLOUT], + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [ + null, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + ROLLOUT_VALUE, + ], + visibleValues: [ + USER_VALUE, + USER_VALUE, + USER_VALUE, + USER_VALUE, + USER_VALUE, + ], + }); + + await doTest({ + configs, + userBranchValue: USER_VALUE, + defaultBranchValue: DEFAULT_VALUE, + enrollOrder: [EXPERIMENT, ROLLOUT], + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [ + DEFAULT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + DEFAULT_VALUE, + ], + visibleValues: [ + USER_VALUE, + USER_VALUE, + USER_VALUE, + USER_VALUE, + USER_VALUE, + ], + }); + } + + // Tests for a feature that sets prefs on the user branch. + { + const branch = USER; + const pref = PREFS[branch]; + const configs = CONFIGS[branch]; + + const doTest = args => doBaseTest({ pref, branch, ...args }); + + // Enroll in rollout then unenroll. + await doTest({ + configs: pick(configs, ROLLOUT), + enrollOrder: [ROLLOUT], + unenrollOrder: [ROLLOUT], + expectedValues: [null, ROLLOUT_VALUE, null], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + configs: pick(configs, ROLLOUT), + enrollOrder: [ROLLOUT], + unenrollOrder: [ROLLOUT], + expectedValues: [null, ROLLOUT_VALUE, null], + visibleValues: [DEFAULT_VALUE, ROLLOUT_VALUE, DEFAULT_VALUE], + }); + + await doTest({ + userBranchValue: USER_VALUE, + configs: pick(configs, ROLLOUT), + enrollOrder: [ROLLOUT], + unenrollOrder: [ROLLOUT], + expectedValues: [USER_VALUE, ROLLOUT_VALUE, USER_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + configs: pick(configs, ROLLOUT), + enrollOrder: [ROLLOUT], + unenrollOrder: [ROLLOUT], + expectedValues: [USER_VALUE, ROLLOUT_VALUE, USER_VALUE], + }); + + // Enroll in experiment then unenroll. + await doTest({ + configs: pick(configs, EXPERIMENT), + enrollOrder: [EXPERIMENT], + unenrollOrder: [EXPERIMENT], + expectedValues: [null, EXPERIMENT_VALUE, null], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + configs: pick(configs, EXPERIMENT), + enrollOrder: [EXPERIMENT], + unenrollOrder: [EXPERIMENT], + expectedValues: [null, EXPERIMENT_VALUE, null], + visibleValues: [DEFAULT_VALUE, EXPERIMENT_VALUE, DEFAULT_VALUE], + }); + + await doTest({ + userBranchValue: USER_VALUE, + configs: pick(configs, EXPERIMENT), + enrollOrder: [EXPERIMENT], + unenrollOrder: [EXPERIMENT], + expectedValues: [USER_VALUE, EXPERIMENT_VALUE, USER_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + configs: pick(configs, EXPERIMENT), + enrollOrder: [EXPERIMENT], + unenrollOrder: [EXPERIMENT], + expectedValues: [USER_VALUE, EXPERIMENT_VALUE, USER_VALUE], + }); + + // Enroll in rollout then experiment; unenroll in reverse order. + await doTest({ + configs, + enrollOrder: [ROLLOUT, EXPERIMENT], + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [ + null, + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + null, + ], + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + enrollOrder: [ROLLOUT, EXPERIMENT], + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [ + null, + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + null, + ], + visibleValues: [ + DEFAULT_VALUE, // User branch falls back to default branch. + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + DEFAULT_VALUE, // User branch falls back to default branch. + ], + }); + + await doTest({ + configs, + userBranchValue: USER_VALUE, + enrollOrder: [ROLLOUT, EXPERIMENT], + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [ + USER_VALUE, + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + USER_VALUE, + ], + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + enrollOrder: [ROLLOUT, EXPERIMENT], + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [ + USER_VALUE, + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + USER_VALUE, + ], + }); + + // Enroll in rollout then experiment; unenroll in same order. + await doTest({ + configs, + enrollOrder: [ROLLOUT, EXPERIMENT], + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [ + null, + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + null, + ], + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + enrollOrder: [ROLLOUT, EXPERIMENT], + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [ + null, + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + null, + ], + visibleValues: [ + DEFAULT_VALUE, // User branch falls back to default branch. + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + DEFAULT_VALUE, // User branch falls back to default branch. + ], + }); + + await doTest({ + configs, + userBranchValue: USER_VALUE, + enrollOrder: [ROLLOUT, EXPERIMENT], + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [ + USER_VALUE, + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + USER_VALUE, + ], + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + enrollOrder: [ROLLOUT, EXPERIMENT], + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [ + USER_VALUE, + ROLLOUT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + USER_VALUE, + ], + }); + + // Enroll in experiment then rollout; unenroll in reverse order. + await doTest({ + configs, + enrollOrder: [EXPERIMENT, ROLLOUT], + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [ + null, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + null, + ], + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + enrollOrder: [EXPERIMENT, ROLLOUT], + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [ + null, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + null, + ], + visibleValues: [ + DEFAULT_VALUE, // User branch falls back to default branch. + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + DEFAULT_VALUE, // User branch falls back to default branch. + ], + }); + + await doTest({ + configs, + userBranchValue: USER_VALUE, + enrollOrder: [EXPERIMENT, ROLLOUT], + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [ + USER_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + USER_VALUE, + ], + }); + + await doTest({ + configs, + userBranchValue: USER_VALUE, + defaultBranchValue: DEFAULT_VALUE, + enrollOrder: [EXPERIMENT, ROLLOUT], + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [ + USER_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + USER_VALUE, + ], + }); + + // Enroll in experiment then rollout; unenroll in same order. + await doTest({ + configs, + enrollOrder: [EXPERIMENT, ROLLOUT], + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [ + null, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + null, + ], + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + enrollOrder: [EXPERIMENT, ROLLOUT], + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [ + null, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + null, + ], + visibleValues: [ + DEFAULT_VALUE, // User branch falls back to default branch. + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + DEFAULT_VALUE, // User branch falls back to default branch. + ], + }); + + await doTest({ + configs, + userBranchValue: USER_VALUE, + enrollOrder: [EXPERIMENT, ROLLOUT], + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [ + USER_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + USER_VALUE, + ], + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + enrollOrder: [EXPERIMENT, ROLLOUT], + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [ + USER_VALUE, + EXPERIMENT_VALUE, + EXPERIMENT_VALUE, + ROLLOUT_VALUE, + USER_VALUE, + ], + }); + } + + await cleanupStore(store); +}); + +add_task(async function test_restorePrefs_experimentAndRollout() { + /** + * Test that prefs are set correctly after restoring from a saved store file + * and unrnollment. + * + * This test sets up some enrollments and saves them to disk. + * + * A browser restart will be simulated by creating a new ExperimentStore and + * ExperimentManager to restore the saved enrollments. + * + * @param {object} options + * @param {string} options.pref + * The name of the pref. + * + * @param {string} options.branch + * The name of the pref branch ("user" or "default"). + * + * @param {object} options.configs + * The rollout and experiment feature configurations. + * + * @param {string?} options.defaultBranchValue + * An optional value to set for the pref on the default branch + * before the first enrollment. + * + * @param {string?} options.userBranchValue + * An optional value to set for the pref on the user branch + * before the first enrollment. + * + * @param {string[]} options.unenrollOrder + * An optional value to set for the pref on the default branch + * before the first enrollment. + * + * @param {(string|null)[]} options.expectedValues + * The expected values of the preft on the given branch at each point: + * + * * before enrollment; + * * one entry each each after enrolling in `options.enrollOrder[i]`; and + * * one entry each each after unenrolling in `options.unenrollOrder[i]`. + * + * A value of null indicates that the pref should not be set on that + * branch. + * + * @param {(string|null)[]?} options.visibleValues + * The expected values returned by + * Services.prefs.getStringPref (i.e., the user branch if set, + * falling back to the default branch if not), in the same + * order as `options.expectedValues`. + * + * If undefined, then it will default to `options.expectedValues`. + */ + async function doBaseTest({ + featureId, + pref, + branch, + configs, + defaultBranchValue = null, + userBranchValue = null, + unenrollOrder, + expectedValues, + visibleValues = undefined, + }) { + if (![USER, DEFAULT].includes(branch)) { + Assert.ok(false, `invalid branch ${branch}`); + } + + if (visibleValues === undefined) { + visibleValues = expectedValues; + } + + // Set the initial conditions. + setPrefs(pref, { defaultBranchValue, userBranchValue }); + + // Enroll in some experiments and save the state to disk. + { + const store = ExperimentFakes.store(); + const manager = ExperimentFakes.manager(store); + + await manager.onStartup(); + + assertEmptyStore(store); + + for (const [enrollmentKind, config] of Object.entries(configs)) { + await ExperimentFakes.enrollWithFeatureConfig(config, { + manager, + isRollout: enrollmentKind === ROLLOUT, + }); + } + + store._store.saveSoon(); + await store._store.finalize(); + + // User branch prefs persist through restart, so we only want to delete + // the prefs if we changed the default branch. + if (branch === "default") { + Services.prefs.deleteBranch(pref); + } + + removePrefObservers(manager); + assertNoObservers(manager); + } + + // Restore the default branch value as it was before "restarting". + setPrefs(pref, { defaultBranchValue }); + // If this is not a user branch pref, restore the user branch value. User + // branch values persist through restart, so we don't want to overwrite a + // value we just set. + if (branch === "default") { + setPrefs(pref, { userBranchValue }); + } + + const sandbox = sinon.createSandbox(); + + const store = ExperimentFakes.store(); + const manager = ExperimentFakes.manager(store); + + const setPrefSpy = sandbox.spy(PrefUtils, "setPref"); + + await manager.onStartup(); + + if (branch === DEFAULT) { + Assert.ok(setPrefSpy.calledOnce, "Should have called setPref once total"); + Assert.ok( + setPrefSpy.calledOnceWith(pref, expectedValues[0], { branch }), + `Should have only called setPref with correct args (called with: ${JSON.stringify( + setPrefSpy.getCall(0).args + )}) expected ${JSON.stringify([pref, expectedValues[0], { branch }])})` + ); + } else if (branch === USER) { + Assert.ok( + setPrefSpy.notCalled, + "Should have not called setPref for a user branch pref" + ); + } + + assertExpectedPrefValues( + pref, + branch, + expectedValues[0], + visibleValues[0], + "after manager startup" + ); + + const slugs = { + [ROLLOUT]: store.getRolloutForFeature(featureId)?.slug, + [EXPERIMENT]: store.getExperimentForFeature(featureId)?.slug, + }; + + let i = 1; + for (const enrollmentKind of unenrollOrder) { + manager.unenroll(slugs[enrollmentKind]); + + assertExpectedPrefValues( + pref, + branch, + expectedValues[i], + visibleValues[i], + `after ${enrollmentKind} unenrollment` + ); + + i++; + } + + for (const enrollmentKind of unenrollOrder) { + // The unenrollment happened normally, not through a cleanup function. + store._deleteForTests(slugs[enrollmentKind]); + } + + assertNoObservers(manager); + assertEmptyStore(store); + await cleanupStore(store); + + Services.prefs.deleteBranch(pref); + sandbox.restore(); + } + + { + const branch = DEFAULT; + const featureId = FEATURE_IDS[branch]; + const pref = PREFS[branch]; + const configs = CONFIGS[branch]; + + const doTest = args => doBaseTest({ featureId, pref, branch, ...args }); + + // Tests with no prefs set beforehand. + await doTest({ + configs: pick(configs, EXPERIMENT), + unenrollOrder: [EXPERIMENT], + expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE], + }); + + await doTest({ + configs: pick(configs, ROLLOUT), + unenrollOrder: [ROLLOUT], + expectedValues: [ROLLOUT_VALUE, ROLLOUT_VALUE], + }); + + await doTest({ + configs, + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, ROLLOUT_VALUE], + }); + + await doTest({ + configs, + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, EXPERIMENT_VALUE], + }); + + // Tests where the default branch is set beforehand. + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + configs: pick(configs, EXPERIMENT), + unenrollOrder: [EXPERIMENT], + expectedValues: [EXPERIMENT_VALUE, DEFAULT_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + configs: pick(configs, ROLLOUT), + unenrollOrder: [ROLLOUT], + expectedValues: [ROLLOUT_VALUE, DEFAULT_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + configs, + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, DEFAULT_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + configs, + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, DEFAULT_VALUE], + }); + + // Tests where the user branch is set beforehand. + + await doTest({ + userBranchValue: USER_VALUE, + configs: pick(configs, EXPERIMENT), + unenrollOrder: [EXPERIMENT], + expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE], + visibleValues: [USER_VALUE, USER_VALUE], + }); + + await doTest({ + userBranchValue: USER_VALUE, + configs: pick(configs, ROLLOUT), + unenrollOrder: [ROLLOUT], + expectedValues: [ROLLOUT_VALUE, ROLLOUT_VALUE], + visibleValues: [USER_VALUE, USER_VALUE], + }); + + await doTest({ + userBranchValue: USER_VALUE, + configs, + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, ROLLOUT_VALUE], + visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE], + }); + + await doTest({ + userBranchValue: USER_VALUE, + configs, + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, EXPERIMENT_VALUE], + visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE], + }); + + // Tests with both branches set beforehand + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + configs: pick(configs, EXPERIMENT), + unenrollOrder: [EXPERIMENT], + expectedValues: [EXPERIMENT_VALUE, DEFAULT_VALUE], + visibleValues: [USER_VALUE, USER_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + configs: pick(configs, ROLLOUT), + unenrollOrder: [ROLLOUT], + expectedValues: [ROLLOUT_VALUE, DEFAULT_VALUE], + visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + configs, + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, DEFAULT_VALUE], + visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + configs, + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, DEFAULT_VALUE], + visibleValues: [USER_VALUE, USER_VALUE, USER_VALUE], + }); + } + + { + const branch = USER; + const featureId = FEATURE_IDS[branch]; + const pref = PREFS[branch]; + const configs = CONFIGS[branch]; + + const doTest = args => + doBaseTest({ featureId, pref, branch, configs, ...args }); + + // Tests with no prefs set beforehand. + await doTest({ + configs: pick(configs, EXPERIMENT), + unenrollOrder: [EXPERIMENT], + expectedValues: [EXPERIMENT_VALUE, null], + }); + + await doTest({ + configs: pick(configs, ROLLOUT), + unenrollOrder: [ROLLOUT], + expectedValues: [ROLLOUT_VALUE, null], + }); + + await doTest({ + configs, + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, null], + }); + + await doTest({ + configs, + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, null], + }); + + // Tests with the default branch set beforehand. + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + configs: pick(configs, EXPERIMENT), + unenrollOrder: [EXPERIMENT], + expectedValues: [EXPERIMENT_VALUE, null], + visibleValues: [EXPERIMENT_VALUE, DEFAULT_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + configs: pick(configs, ROLLOUT), + unenrollOrder: [ROLLOUT], + expectedValues: [ROLLOUT_VALUE, null], + visibleValues: [ROLLOUT_VALUE, DEFAULT_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + configs, + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, null], + visibleValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, DEFAULT_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + configs, + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, null], + visibleValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, DEFAULT_VALUE], + }); + + // Tests with the user branch set beforehand. + await doTest({ + userBranchValue: USER_VALUE, + configs: pick(configs, EXPERIMENT), + unenrollOrder: [EXPERIMENT], + expectedValues: [EXPERIMENT_VALUE, USER_VALUE], + }); + + await doTest({ + userBranchValue: USER_VALUE, + configs: pick(configs, ROLLOUT), + unenrollOrder: [ROLLOUT], + expectedValues: [ROLLOUT_VALUE, USER_VALUE], + }); + + await doTest({ + userBranchValue: USER_VALUE, + configs, + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, USER_VALUE], + }); + + await doTest({ + userBranchValue: USER_VALUE, + configs, + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, USER_VALUE], + }); + + // Tests with both branches set beforehand + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + configs: pick(configs, EXPERIMENT), + unenrollOrder: [EXPERIMENT], + expectedValues: [EXPERIMENT_VALUE, USER_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + configs: pick(configs, ROLLOUT), + unenrollOrder: [ROLLOUT], + expectedValues: [ROLLOUT_VALUE, USER_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + configs, + unenrollOrder: [EXPERIMENT, ROLLOUT], + expectedValues: [EXPERIMENT_VALUE, ROLLOUT_VALUE, USER_VALUE], + }); + + await doTest({ + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + configs, + unenrollOrder: [ROLLOUT, EXPERIMENT], + expectedValues: [EXPERIMENT_VALUE, EXPERIMENT_VALUE, USER_VALUE], + }); + } +}); + +add_task(async function test_prefChange() { + /** + * Test that pref tampering causes unenrollment. + * + * This test sets up some enrollments and then modifies the given `pref` on a + * branch specified by `setBranch` and checks that unenrollments happen as + * appropriate. + * + * @param {object} options + * + * @param {string} options.pref + * The name of the pref. + * + * @param {string?} options.defaultBranchValue + * An optional value to set for the pref on the default branch + * before the first enrollment. + * + * @param {string?} options.userBranchValue + * An optional value to set for the pref on the user branch + * before the first enrollment. + * + * @param {object} options.configs + * The rollout and experiment feature configurations. + * + * @param {string} options.setBranch + * The branch that the test will set (either "user" or "default"). + * + * @param {string[]} options.expectedEnrollments + * The list of enrollment kinds (e.g., "rollout" or "experiment") that + * should be active after setting the pref on the requested branch. + * + * @param {string} options.expectedDefault + * The expected value of the default branch after setting the pref on + * the requested branch. + * + * A value of null indicates that the pref should not be set on the + * default branch. + * + * @param {string} options.expectedUser + * The expected value of the user branch after setting the pref on the + * requested branch. + * + * A value of null indicates that the pref should not be set on the + * user branch. + */ + async function doBaseTest({ + pref, + defaultBranchValue = null, + userBranchValue = null, + configs, + setBranch, + expectedEnrollments = [], + expectedDefault = null, + expectedUser = null, + }) { + const store = ExperimentFakes.store(); + const manager = ExperimentFakes.manager(store); + + const cleanup = {}; + const slugs = {}; + + await manager.onStartup(); + + assertEmptyStore(store); + + setPrefs(pref, { defaultBranchValue, userBranchValue }); + + info(`Enrolling in ${Array.from(Object.keys(configs)).join(", ")} ...`); + for (const [enrollmentKind, config] of Object.entries(configs)) { + const isRollout = enrollmentKind === ROLLOUT; + cleanup[enrollmentKind] = await ExperimentFakes.enrollWithFeatureConfig( + config, + { + manager, + isRollout, + } + ); + + const enrollments = isRollout + ? store.getAllRollouts() + : store.getAllActive(); + + Assert.equal( + enrollments.length, + 1, + `Expected one ${enrollmentKind} enrollment` + ); + slugs[enrollmentKind] = enrollments[0].slug; + } + + info( + `Overwriting ${pref} with "${OVERWRITE_VALUE}" on ${setBranch} branch` + ); + + PrefUtils.setPref(pref, OVERWRITE_VALUE, { branch: setBranch }); + + if (expectedDefault === null) { + Assert.ok( + !Services.prefs.prefHasDefaultValue(pref), + `Expected the default branch not to be set for ${pref}` + ); + } else { + Assert.equal( + Services.prefs.getDefaultBranch(null).getStringPref(pref), + expectedDefault, + `Expected the value of ${pref} on the default branch to match the expected value` + ); + } + + if (expectedUser === null) { + Assert.ok( + !Services.prefs.prefHasUserValue(pref), + `Expected the user branch not to be set for ${pref}` + ); + } else { + Assert.equal( + Services.prefs.getStringPref(pref), + expectedUser, + `Expected the value of ${pref} on the user branch to match the expected value` + ); + } + + for (const enrollmentKind of expectedEnrollments) { + const enrollment = store.get(slugs[enrollmentKind]); + + Assert.ok( + enrollment !== null, + `An enrollment of kind ${enrollmentKind} should exist` + ); + Assert.ok(enrollment.active, "It should still be active"); + } + + for (const enrollmentKind of Object.keys(configs)) { + if (!expectedEnrollments.includes(enrollmentKind)) { + const slug = slugs[enrollmentKind]; + const enrollment = store.get(slug); + + Assert.ok( + enrollment !== null, + `An enrollment of kind ${enrollmentKind} should exist` + ); + Assert.ok(!enrollment.active, "It should not be active"); + + store._deleteForTests(slug); + } + } + + for (const enrollmentKind of expectedEnrollments) { + await cleanup[enrollmentKind](); + } + + assertNoObservers(manager); + assertEmptyStore(store); + await cleanupStore(store); + + Services.prefs.deleteBranch(pref); + } + + { + const branch = DEFAULT; + const pref = PREFS[branch]; + const configs = CONFIGS[branch]; + + const doTest = args => doBaseTest({ pref, branch, ...args }); + + // Enrolled in rollout, set default branch. + await doTest({ + configs: pick(configs, ROLLOUT), + setBranch: DEFAULT, + expectedDefault: OVERWRITE_VALUE, + }); + + await doTest({ + configs: pick(configs, ROLLOUT), + defaultBranchValue: DEFAULT_VALUE, + setBranch: DEFAULT, + expectedDefault: OVERWRITE_VALUE, + }); + + await doTest({ + configs: pick(configs, ROLLOUT), + userBranchValue: USER_VALUE, + setBranch: DEFAULT, + expectedEnrollments: [ROLLOUT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: USER_VALUE, + }); + + await doTest({ + configs: pick(configs, ROLLOUT), + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + setBranch: DEFAULT, + expectedEnrollments: [ROLLOUT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: USER_VALUE, + }); + + // Enrolled in rollout, set user branch. + await doTest({ + configs: pick(configs, ROLLOUT), + setBranch: USER, + expectedDefault: ROLLOUT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs: pick(configs, ROLLOUT), + defaultBranchValue: DEFAULT_VALUE, + setBranch: USER, + expectedDefault: DEFAULT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs: pick(configs, ROLLOUT), + userBranchValue: USER_VALUE, + setBranch: USER, + expectedDefault: ROLLOUT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs: pick(configs, ROLLOUT), + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + setBranch: USER, + expectedDefault: DEFAULT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + + // Enrolled in experiment, set default branch. + await doTest({ + configs: pick(configs, EXPERIMENT), + setBranch: DEFAULT, + expectedDefault: OVERWRITE_VALUE, + }); + + await doTest({ + configs: pick(configs, EXPERIMENT), + defaultBranchValue: DEFAULT_VALUE, + setBranch: DEFAULT, + expectedDefault: OVERWRITE_VALUE, + }); + + await doTest({ + configs: pick(configs, EXPERIMENT), + userBranchValue: USER_VALUE, + setBranch: DEFAULT, + expectedEnrollments: [EXPERIMENT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: USER_VALUE, + }); + + await doTest({ + configs: pick(configs, EXPERIMENT), + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + setBranch: DEFAULT, + expectedEnrollments: [EXPERIMENT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: USER_VALUE, + }); + + // Enrolled in experiment, set user branch. + await doTest({ + configs: pick(configs, EXPERIMENT), + setBranch: USER, + expectedDefault: EXPERIMENT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs: pick(configs, EXPERIMENT), + defaultBranchValue: DEFAULT_VALUE, + setBranch: USER, + expectedDefault: DEFAULT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs: pick(configs, EXPERIMENT), + userBranchValue: USER_VALUE, + setBranch: USER, + expectedDefault: EXPERIMENT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs: pick(configs, EXPERIMENT), + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + setBranch: USER, + expectedDefault: DEFAULT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + + // Enroll in both, set default branch. + await doTest({ + configs, + setBranch: DEFAULT, + expectedDefault: OVERWRITE_VALUE, + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + setBranch: DEFAULT, + expectedDefault: OVERWRITE_VALUE, + }); + + await doTest({ + configs, + userBranchValue: USER_VALUE, + setBranch: DEFAULT, + expectedEnrollments: [EXPERIMENT, ROLLOUT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: USER_VALUE, + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + setBranch: DEFAULT, + expectedEnrollments: [EXPERIMENT, ROLLOUT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: USER_VALUE, + }); + + // Enroll in both, set user branch. + await doTest({ + configs, + setBranch: USER, + expectedDefault: EXPERIMENT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + setBranch: USER, + expectedDefault: DEFAULT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs, + userBranchValue: USER_VALUE, + setBranch: USER, + expectedDefault: EXPERIMENT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + setBranch: USER, + expectedDefault: DEFAULT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + } + + { + const branch = USER; + const pref = PREFS[branch]; + const configs = CONFIGS[branch]; + + const doTest = args => doBaseTest({ pref, branch, ...args }); + + // Enrolled in rollout, set default branch. + await doTest({ + configs: pick(configs, ROLLOUT), + setBranch: DEFAULT, + expectedEnrollments: [ROLLOUT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: ROLLOUT_VALUE, + }); + + await doTest({ + configs: pick(configs, ROLLOUT), + defaultBranchValue: DEFAULT_VALUE, + setBranch: DEFAULT, + expectedEnrollments: [ROLLOUT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: ROLLOUT_VALUE, + }); + + await doTest({ + configs: pick(configs, ROLLOUT), + userBranchValue: USER_VALUE, + setBranch: DEFAULT, + expectedEnrollments: [ROLLOUT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: ROLLOUT_VALUE, + }); + + await doTest({ + configs: pick(configs, ROLLOUT), + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + setBranch: DEFAULT, + expectedEnrollments: [ROLLOUT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: ROLLOUT_VALUE, + }); + + // Enrolled in rollout, set user branch. + await doTest({ + configs: pick(configs, ROLLOUT), + setBranch: USER, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs: pick(configs, ROLLOUT), + defaultBranchValue: DEFAULT_VALUE, + setBranch: USER, + expectedDefault: DEFAULT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs: pick(configs, ROLLOUT), + userBranchValue: USER_VALUE, + setBranch: USER, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs: pick(configs, ROLLOUT), + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + setBranch: USER, + expectedDefault: DEFAULT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + + // Enrolled in experiment, set default branch. + await doTest({ + configs: pick(configs, EXPERIMENT), + setBranch: DEFAULT, + expectedEnrollments: [EXPERIMENT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: EXPERIMENT_VALUE, + }); + + await doTest({ + configs: pick(configs, EXPERIMENT), + defaultBranchValue: DEFAULT_VALUE, + setBranch: DEFAULT, + expectedEnrollments: [EXPERIMENT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: EXPERIMENT_VALUE, + }); + + await doTest({ + configs: pick(configs, EXPERIMENT), + userBranchValue: USER_VALUE, + setBranch: DEFAULT, + expectedEnrollments: [EXPERIMENT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: EXPERIMENT_VALUE, + }); + + await doTest({ + configs: pick(configs, EXPERIMENT), + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + setBranch: DEFAULT, + expectedEnrollments: [EXPERIMENT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: EXPERIMENT_VALUE, + }); + + // Enrolled in experiment, set user branch. + await doTest({ + configs: pick(configs, EXPERIMENT), + setBranch: USER, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs: pick(configs, EXPERIMENT), + defaultBranchValue: DEFAULT_VALUE, + setBranch: USER, + expectedDefault: DEFAULT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs: pick(configs, EXPERIMENT), + userBranchValue: USER_VALUE, + setBranch: USER, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs: pick(configs, EXPERIMENT), + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + setBranch: USER, + expectedDefault: DEFAULT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + + // Enrolled in both, set default branch. + await doTest({ + configs, + setBranch: DEFAULT, + expectedEnrollments: [EXPERIMENT, ROLLOUT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: EXPERIMENT_VALUE, + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + setBranch: DEFAULT, + expectedEnrollments: [EXPERIMENT, ROLLOUT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: EXPERIMENT_VALUE, + }); + + await doTest({ + configs, + userBranchValue: USER_VALUE, + setBranch: DEFAULT, + expectedEnrollments: [EXPERIMENT, ROLLOUT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: EXPERIMENT_VALUE, + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + setBranch: DEFAULT, + expectedEnrollments: [EXPERIMENT, ROLLOUT], + expectedDefault: OVERWRITE_VALUE, + expectedUser: EXPERIMENT_VALUE, + }); + + // Enrolled in both, set user branch. + await doTest({ + configs, + setBranch: USER, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + setBranch: USER, + expectedDefault: DEFAULT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs, + userBranchValue: USER_VALUE, + setBranch: USER, + expectedUser: OVERWRITE_VALUE, + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + setBranch: USER, + expectedDefault: DEFAULT_VALUE, + expectedUser: OVERWRITE_VALUE, + }); + } +}); + +add_task(async function test_deleteBranch() { + const store = ExperimentFakes.store(); + const manager = ExperimentFakes.manager(store); + + await manager.onStartup(); + + assertEmptyStore(store); + + const cleanup = []; + cleanup.push( + await ExperimentFakes.enrollWithFeatureConfig(CONFIGS[USER][EXPERIMENT], { + manager, + }), + await ExperimentFakes.enrollWithFeatureConfig(CONFIGS[USER][ROLLOUT], { + manager, + isRollout: true, + }), + await ExperimentFakes.enrollWithFeatureConfig( + CONFIGS[DEFAULT][EXPERIMENT], + { manager } + ), + await ExperimentFakes.enrollWithFeatureConfig(CONFIGS[DEFAULT][ROLLOUT], { + manager, + isRollout: true, + }) + ); + + Services.prefs.deleteBranch(PREFS[USER]); + Services.prefs.deleteBranch(PREFS[DEFAULT]); + + // deleteBranch does not trigger pref observers! + Assert.equal( + store.getAll().length, + 4, + "nsIPrefBranch::deleteBranch does not trigger unenrollment" + ); + + for (const cleanupFn of cleanup) { + await cleanupFn(); + } + + assertNoObservers(manager); + assertEmptyStore(store); + await cleanupStore(store); +}); + +add_task(async function test_clearUserPref() { + /** + * Test that nsIPrefBranch::clearUserPref() correctly interacts with pref + * tampering logic. + * + * This test sets up some enrollments and then clears the pref specified and + * checks that unenrollments happen as * appropriate. + * + * @param {object} options + * + * @param {string} options.pref + * The name of the pref. + * + * @param {string?} options.defaultBranchValue + * An optional value to set for the pref on the default branch + * before the first enrollment. + * + * @param {string?} options.userBranchValue + * An optional value to set for the pref on the user branch + * before the first enrollment. + * + * @param {object} options.configs + * The rollout and experiment feature configurations. + * + * @param {boolean} options.expectedEnrolled + * Whether or not the enrollments defined in `configs` should still be + * active after clearing the user branch. + * + * @param {string} options.expectedDefault + * The expected value of the default branch after clearing the user branch. + * + * A value of null indicates that the pref should not be set on the default + * branch. + */ + async function doBaseTest({ + pref, + defaultBranchValue = null, + userBranchValue = null, + configs, + expectedEnrolled, + expectedDefault = null, + }) { + const store = ExperimentFakes.store(); + const manager = ExperimentFakes.manager(store); + + await manager.onStartup(); + + assertEmptyStore(store); + + const cleanup = []; + const slugs = {}; + + setPrefs(pref, { defaultBranchValue, userBranchValue }); + + for (const [enrollmentKind, config] of Object.entries(configs)) { + const isRollout = enrollmentKind === ROLLOUT; + cleanup.push( + await ExperimentFakes.enrollWithFeatureConfig(config, { + manager, + isRollout, + }) + ); + + const enrollments = isRollout + ? store.getAllRollouts() + : store.getAllActive(); + + Assert.equal( + enrollments.length, + 1, + `Expected one ${enrollmentKind} enrollment` + ); + slugs[enrollmentKind] = enrollments[0].slug; + } + + Services.prefs.clearUserPref(pref); + + for (const enrollmentKind of Object.keys(configs)) { + const slug = slugs[enrollmentKind]; + const enrollment = store.get(slug); + Assert.ok( + enrollment !== null, + `An enrollment of kind ${enrollmentKind} should exist` + ); + + if (expectedEnrolled) { + Assert.ok(enrollment.active, "It should be active"); + } else { + Assert.ok(!enrollment.active, "It should not be active"); + } + } + + if (expectedDefault === null) { + Assert.ok( + !Services.prefs.prefHasDefaultValue(pref), + `Expected the default branch not to be set for ${pref}` + ); + } else { + Assert.equal( + Services.prefs.getDefaultBranch(null).getStringPref(pref), + expectedDefault, + `Expected the value of ${pref} on the default branch to match the expected value` + ); + } + + Assert.ok( + !Services.prefs.prefHasUserValue(pref), + `Expected the user branch not to be set for ${pref}` + ); + + if (expectedEnrolled) { + for (const cleanupFn of Object.values(cleanup)) { + await cleanupFn(); + } + } else { + for (const slug of Object.values(slugs)) { + store._deleteForTests(slug); + } + } + + assertNoObservers(manager); + assertEmptyStore(store); + await cleanupStore(store); + + Services.prefs.deleteBranch(pref); + } + + { + const branch = DEFAULT; + const pref = PREFS[branch]; + const configs = CONFIGS[branch]; + const doTest = args => doBaseTest({ pref, branch, ...args }); + + // Enroll in rollout. + await doTest({ + configs: pick(configs, ROLLOUT), + expectedEnrolled: true, + expectedDefault: ROLLOUT_VALUE, + }); + + await doTest({ + configs: pick(configs, ROLLOUT), + defaultBranchValue: DEFAULT_VALUE, + expectedEnrolled: true, + expectedDefault: ROLLOUT_VALUE, + }); + + await doTest({ + configs: pick(configs, ROLLOUT), + userBranchValue: USER_VALUE, + expectedEnrolled: false, + expectedDefault: ROLLOUT_VALUE, + }); + + await doTest({ + configs: pick(configs, ROLLOUT), + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + expectedEnrolled: false, + expectedDefault: DEFAULT_VALUE, + }); + + // Enroll in experiment. + await doTest({ + configs: pick(configs, EXPERIMENT), + expectedEnrolled: true, + expectedDefault: EXPERIMENT_VALUE, + }); + + await doTest({ + configs: pick(configs, EXPERIMENT), + defaultBranchValue: DEFAULT_VALUE, + expectedEnrolled: true, + expectedDefault: EXPERIMENT_VALUE, + }); + + await doTest({ + configs: pick(configs, EXPERIMENT), + userBranchValue: USER_VALUE, + expectedEnrolled: false, + expectedDefault: EXPERIMENT_VALUE, + }); + + await doTest({ + configs: pick(configs, EXPERIMENT), + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + expectedEnrolled: false, + expectedDefault: DEFAULT_VALUE, + }); + + // Enroll in both. + await doTest({ + configs, + expectedEnrolled: true, + expectedDefault: EXPERIMENT_VALUE, + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + expectedEnrolled: true, + expectedDefault: EXPERIMENT_VALUE, + }); + + await doTest({ + configs, + userBranchValue: USER_VALUE, + expectedEnrolled: false, + expectedDefault: EXPERIMENT_VALUE, + }); + + await doTest({ + configs, + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + expectedEnrolled: false, + expectedDefault: DEFAULT_VALUE, + }); + } + + { + const branch = USER; + const pref = PREFS[branch]; + const configs = CONFIGS[branch]; + const doTest = args => + doBaseTest({ pref, branch, expectedEnrolled: false, ...args }); + + // Because this pref is set on the user branch, clearing the user branch has + // the same effect for every suite of configs. + for (const selectedConfig of [ + pick(configs, ROLLOUT), + pick(configs, EXPERIMENT), + configs, + ]) { + await doTest({ + configs: selectedConfig, + }); + + await doTest({ + configs: selectedConfig, + defaultBranchValue: DEFAULT_VALUE, + expectedDefault: DEFAULT_VALUE, + }); + + await doTest({ + configs: selectedConfig, + userBranchValue: USER_VALUE, + }); + + await doTest({ + configs: selectedConfig, + defaultBranchValue: DEFAULT_VALUE, + userBranchValue: USER_VALUE, + expectedDefault: DEFAULT_VALUE, + }); + } + } +}); + +// Test that unenrollment doesn't happen if a pref changes but it wasn't set. +add_task(async function test_prefChanged_noPrefSet() { + const featureId = "test-set-pref-2"; + const pref = "nimbus.test-only.baz"; + + function featureFactory(isEarlyStartup) { + return new ExperimentFeature(featureId, { + description: "Test feature that sets a pref", + owner: "test@test.test", + hasExposure: false, + isEarlyStartup, + variables: { + baz: { + type: "string", + description: "Test variable", + setPref: pref, + }, + qux: { + type: "string", + description: "Test variable", + }, + }, + }); + } + + const config = { + featureId, + value: { + qux: "qux", + }, + }; + + for (const isEarlyStartup of [true, false]) { + const feature = featureFactory(isEarlyStartup); + const cleanupFeature = ExperimentTestUtils.addTestFeatures(feature); + + const store = ExperimentFakes.store(); + const manager = ExperimentFakes.manager(store); + await manager.onStartup(); + + for (const branch of [USER, DEFAULT]) { + for (const defaultBranchValue of [null, DEFAULT_VALUE]) { + for (const userBranchValue of [null, USER_VALUE]) { + for (const isRollout of [true, false]) { + setPrefs(pref, { defaultBranchValue, userBranchValue }); + + const doEnrollmentCleanup = await ExperimentFakes.enrollWithFeatureConfig( + config, + { manager, isRollout } + ); + + PrefUtils.setPref(pref, OVERWRITE_VALUE, { branch }); + + const enrollments = await store.getAll(); + Assert.equal( + enrollments.length, + 1, + "There should be one enrollment" + ); + Assert.ok(enrollments[0].active, "The enrollment should be active"); + + Assert.equal( + PrefUtils.getPref(pref, { branch }), + OVERWRITE_VALUE, + `The value of ${pref} on the ${branch} branch should be the expected value` + ); + + if (branch === USER) { + if (defaultBranchValue) { + Assert.equal( + PrefUtils.getPref(pref, { branch: DEFAULT }), + defaultBranchValue, + "The default branch should have the expected value" + ); + } else { + Assert.ok( + !Services.prefs.prefHasDefaultValue(pref), + "The default branch should not have a value" + ); + } + } else if (userBranchValue) { + Assert.equal( + PrefUtils.getPref(pref, { branch: USER }), + userBranchValue, + "The user branch should have the expected value" + ); + } else { + Assert.ok( + !Services.prefs.prefHasUserValue(pref), + "The user branch should not have a value" + ); + } + + assertNoObservers(manager); + + await doEnrollmentCleanup(); + assertEmptyStore(store); + + Services.prefs.deleteBranch(pref); + } + } + } + } + + cleanupFeature(); + await cleanupStore(store); + } +}); + +add_task(async function test_restorePrefs_manifestChanged() { + TelemetryEvents.init(); + + const LEGACY_FILTER = { + category: "normandy", + method: "unenroll", + object: "nimbus_experiment", + }; + + const BOGUS_PREF = "nimbus.test-only.bogus"; + + const REMOVE_FEATURE = "remove-feature"; + const REMOVE_PREF_VARIABLE = "remove-pref-variable"; + const REMOVE_OTHER_VARIABLE = "remove-other-variable"; + const REMOVE_SETPREF = "remove-setpref"; + const CHANGE_SETPREF = "change-setpref"; + + const OPERATIONS = [ + REMOVE_FEATURE, + REMOVE_PREF_VARIABLE, + REMOVE_OTHER_VARIABLE, + REMOVE_SETPREF, + CHANGE_SETPREF, + ]; + + const REASONS = { + [REMOVE_FEATURE]: "invalid-feature", + [REMOVE_PREF_VARIABLE]: "pref-variable-missing", + [REMOVE_SETPREF]: "pref-variable-no-longer", + [CHANGE_SETPREF]: "pref-variable-changed", + }; + + const featureId = "test-set-pref-temp"; + const pref = "nimbus.test-only.baz"; + + // Return a new object so we can modified the returned value. + function featureFactory(isEarlyStartup) { + return new ExperimentFeature(featureId, { + description: "Test feature that sets a pref on the default branch.", + owner: "test@test.test", + hasExposure: false, + isEarlyStartup, + variables: { + baz: { + type: "string", + description: "Test variable", + setPref: pref, + }, + qux: { + type: "string", + description: "Test variable", + }, + }, + }); + } + + /* + * Test that enrollments end when the manifest is sufficiently changed and + * that the appropriate telemetry is submitted. + * + * This test sets up some enrollments and saves them to disk. Then the + * manifest will be modified according to `operation`. + * + * A browser restart will be simulated by creating a new ExperimentStore and + * ExperimentManager to restore the saved enrollments. + * + * @param {object} options + * + * @param {string} options.branch + * The name of the pref branch ("user" or "default"). + * + * @param {string?} options.defaultBranchValue + * An optional value to set for the pref on the default branch + * before the first enrollment. + * + * @param {string?} options.userBranchValue + * An optional value to set for the pref on the user branch + * before the first enrollment. + * + * @param {object} options.configs + * The rollout and experiment feature configurations. + * + * @param {string} options.operation + * The operation that will be performed on the manifest. + * + * See `OPERATIONS` above. + * + * @param {string[]} options.expectedEnrollments + * The list of enrollment kinds (e.g., "rollout" or "experiment") that + * should be active after setting the pref on the requested branch. + * + * @param {string} options.expectedDefault + * The expected value of the default branch after restoring enrollments. + * + * A value of null indicates that the pref should not be set on the + * default branch. + * + * @param {string} options.expectedUser + * The expected value of the user branch after restoring enrollments. + * + * A value of null indicates that the pref should not be set on the + * user branch. + */ + async function doBaseTest({ + branch, + defaultBranchValue = null, + userBranchValue = null, + configs, + operation, + expectedEnrollments = [], + expectedDefault = null, + expectedUser = null, + }) { + Services.fog.testResetFOG(); + Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + /* clear = */ true + ); + + const feature = featureFactory(branch === USER); + const cleanupFeatures = ExperimentTestUtils.addTestFeatures(feature); + + setPrefs(pref, { defaultBranchValue, userBranchValue }); + + const slugs = {}; + let userPref = null; + + // Enroll in some experiments and save the state to disk. + { + const store = ExperimentFakes.store(); + const manager = ExperimentFakes.manager(store); + + await manager.onStartup(); + + assertEmptyStore(store); + + for (const [enrollmentKind, config] of Object.entries(configs)) { + const isRollout = enrollmentKind === ROLLOUT; + await ExperimentFakes.enrollWithFeatureConfig(config, { + manager, + isRollout, + }); + + const enrollments = isRollout + ? store.getAllRollouts() + : store.getAllActive(); + + Assert.equal( + enrollments.length, + 1, + `Expected one ${enrollmentKind} enrollment` + ); + slugs[enrollmentKind] = enrollments[0].slug; + } + + store._store.saveSoon(); + await store._store.finalize(); + + // User branch prefs persist through restart, so we only want to delete + // the prefs if we changed the default branch. + if (branch === "user") { + userPref = PrefUtils.getPref(pref, { branch }); + } + + Services.prefs.deleteBranch(pref); + + removePrefObservers(manager); + assertNoObservers(manager); + } + + // Restore the default branch value as it was before "restarting". + setPrefs(pref, { + defaultBranchValue, + userBranchValue: userPref ?? userBranchValue, + }); + + // Mangle the manifest. + switch (operation) { + case REMOVE_FEATURE: + cleanupFeatures(); + break; + + case REMOVE_PREF_VARIABLE: + delete NimbusFeatures[featureId].manifest.variables.baz; + break; + + case REMOVE_OTHER_VARIABLE: + delete NimbusFeatures[featureId].manifest.variables.qux; + break; + + case REMOVE_SETPREF: + delete NimbusFeatures[featureId].manifest.variables.baz.setPref; + break; + + case CHANGE_SETPREF: + NimbusFeatures[featureId].manifest.variables.baz.setPref = BOGUS_PREF; + break; + + default: + Assert.ok(false, "invalid operation"); + } + + const store = ExperimentFakes.store(); + const manager = ExperimentFakes.manager(store); + + await manager.onStartup(); + + for (const enrollmentKind of expectedEnrollments) { + const enrollment = store.get(slugs[enrollmentKind]); + + Assert.ok( + enrollment !== null, + `An experiment of kind ${enrollmentKind} should exist` + ); + Assert.ok(enrollment.active, "It should still be active"); + } + + if (expectedDefault === null) { + Assert.ok( + !Services.prefs.prefHasDefaultValue(pref), + `Expected the default branch not to be set for ${pref} value: ${PrefUtils.getPref( + pref, + { branch: "default" } + )}` + ); + } else { + Assert.equal( + Services.prefs.getDefaultBranch(null).getStringPref(pref), + expectedDefault, + `Expected the value of ${pref} on the default branch to match the expected value` + ); + } + + if (expectedUser === null) { + Assert.ok( + !Services.prefs.prefHasUserValue(pref), + `Expected the user branch not to be set for ${pref} value: ${PrefUtils.getPref( + pref, + { branch: "user" } + )}` + ); + } else { + Assert.equal( + Services.prefs.getStringPref(pref), + expectedUser, + `Expected the value of ${pref} on the user branch to match the expected value` + ); + } + + if (operation === CHANGE_SETPREF) { + Assert.ok( + !Services.prefs.prefHasDefaultValue(BOGUS_PREF), + "The new pref should not have a value on the default branch" + ); + Assert.ok( + !Services.prefs.prefHasUserValue(BOGUS_PREF), + "The new pref should not have a value on the user branch" + ); + } + + for (const enrollmentKind of Object.keys(configs)) { + if (!expectedEnrollments.includes(enrollmentKind)) { + const slug = slugs[enrollmentKind]; + const enrollment = store.get(slug); + + Assert.ok( + enrollment !== null, + `An enrollment of kind ${enrollmentKind} should exist` + ); + Assert.ok(!enrollment.active, "It should not be active"); + + store._deleteForTests(slug); + } + } + + const gleanEvents = Glean.nimbusEvents.unenrollment.testGetValue(); + if (expectedEnrollments.length === 0) { + const expectedEvents = [EXPERIMENT, ROLLOUT] + .filter(enrollmentKind => Object.hasOwn(slugs, enrollmentKind)) + .map(enrollmentKind => ({ + reason: REASONS[operation], + experiment: slugs[enrollmentKind], + })); + + // Extract only the values we care about. + const processedEvents = gleanEvents.map(event => ({ + reason: event.extra.reason, + experiment: event.extra.experiment, + })); + + Assert.deepEqual( + processedEvents, + expectedEvents, + "Glean should have the expected unenrollment events" + ); + + const expectedLegacyEvents = expectedEvents.map(extra => ({ + value: extra.experiment, + extra: pick(extra, "reason"), + })); + + TelemetryTestUtils.assertEvents(expectedLegacyEvents, LEGACY_FILTER); + } else { + Assert.equal( + gleanEvents, + undefined, + "Glean should have no unenrollment events" + ); + + TelemetryTestUtils.assertEvents([], LEGACY_FILTER); + } + + for (const enrollmentKind of expectedEnrollments) { + const slug = slugs[enrollmentKind]; + manager.unenroll(slug); + store._deleteForTests(slug); + } + + assertEmptyStore(store); + await cleanupStore(store); + + assertNoObservers(manager); + Services.prefs.deleteBranch(pref); + + if (operation !== REMOVE_FEATURE) { + // If we try to remove the feature twice, we will throw an exception. + cleanupFeatures(); + } + } + + // Test only qux set. These tests should not cause any unenrollments. + { + const quxConfigs = { + [EXPERIMENT]: { + featureId, + value: { + qux: EXPERIMENT_VALUE, + }, + }, + [ROLLOUT]: { + featureId, + value: { + qux: ROLLOUT_VALUE, + }, + }, + }; + + const doTest = ({ + branch, + defaultBranchValue = null, + userBranchValue = null, + configs, + operation, + }) => + doBaseTest({ + branch, + configs, + defaultBranchValue, + userBranchValue, + operation, + expectedEnrollments: Object.keys(configs), + expectedDefault: defaultBranchValue, + expectedUser: userBranchValue, + }); + + for (const branch of [USER, DEFAULT]) { + for (const defaultBranchValue of [null, DEFAULT_VALUE]) { + for (const userBranchValue of [null, USER_VALUE]) { + for (const specifiedConfigs of [ + pick(quxConfigs, ROLLOUT), + pick(quxConfigs, EXPERIMENT), + quxConfigs, + ]) { + for (const operation of OPERATIONS) { + await doTest({ + branch, + defaultBranchValue, + userBranchValue, + configs: specifiedConfigs, + operation, + }); + } + } + } + } + } + } + + // Test only baz set. All operations except REMOVE_OTHER_VARIABLE will trigger + // unenrollment. + { + const bazConfigs = { + [EXPERIMENT]: { + featureId, + value: { + baz: EXPERIMENT_VALUE, + }, + }, + [ROLLOUT]: { + featureId, + value: { + baz: ROLLOUT_VALUE, + }, + }, + }; + + const doTest = ({ + branch, + defaultBranchValue = null, + userBranchValue = null, + configs, + operation, + }) => { + const expectedEnrollments = + operation === REMOVE_OTHER_VARIABLE ? Object.keys(configs) : []; + + function expectedPref(forBranch, originalValue) { + if (forBranch === branch) { + if (expectedEnrollments.includes(EXPERIMENT)) { + return EXPERIMENT_VALUE; + } else if (expectedEnrollments.includes(ROLLOUT)) { + return ROLLOUT_VALUE; + } + } + return originalValue; + } + + const expectedDefault = expectedPref(DEFAULT, defaultBranchValue); + const expectedUser = expectedPref(USER, userBranchValue); + + return doBaseTest({ + branch, + configs, + defaultBranchValue, + userBranchValue, + operation, + expectedEnrollments, + expectedDefault, + expectedUser, + }); + }; + + for (const branch of [USER, DEFAULT]) { + for (const defaultBranchValue of [null, DEFAULT_VALUE]) { + for (const userBranchValue of [null, USER_VALUE]) { + for (const specifiedConfigs of [ + pick(bazConfigs, ROLLOUT), + pick(bazConfigs, EXPERIMENT), + bazConfigs, + ]) { + for (const operation of OPERATIONS) { + await doTest({ + branch, + defaultBranchValue, + userBranchValue, + configs: specifiedConfigs, + operation, + }); + } + } + } + } + } + } + + Services.fog.testResetFOG(); + Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + /* clear = */ true + ); +}); |