/* 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 ); });