/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ const { ObjectUtils } = ChromeUtils.importESModule( "resource://gre/modules/ObjectUtils.sys.mjs" ); const { PrefUtils } = ChromeUtils.importESModule( "resource://normandy/lib/PrefUtils.sys.mjs" ); const { ProfilesDatastoreService } = ChromeUtils.importESModule( "moz-src:///toolkit/profile/ProfilesDatastoreService.sys.mjs" ); function assertIncludes(array, obj, msg) { let found = false; for (const el of array) { if (ObjectUtils.deepEqual(el, obj)) { found = true; break; } } Assert.ok(found, msg); } /** * 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, variables: { foo: { type: "string", description: "Test variable", setPref: { branch: "default", pref: "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: { branch: "user", pref: "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}`; } switch (branch) { case USER: Assert.equal( Services.prefs.prefHasUserValue(pref), hasBranchValue, hasValueMsg(hasBranchValue) ); break; case DEFAULT: Assert.equal( Services.prefs.prefHasDefaultValue(pref), hasBranchValue, hasValueMsg(hasBranchValue) ); break; default: Assert.ok(false, "invalid pref branch"); } if (hasBranchValue) { Assert.equal( PrefUtils.getPref(pref, { branch }), expected, `Expected pref "${pref} on the ${branch} branch to be ${JSON.stringify( expected )} ${msg}` ); } if (hasVisibleValue) { Assert.equal( PrefUtils.getPref(pref, { branch: USER }) ?? PrefUtils.getPref(pref, { branch: DEFAULT }), 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}` ); } } add_setup(function setup() { Services.fog.initializeFOG(); registerCleanupFunction(NimbusTestUtils.addTestFeatures(...PREF_FEATURES)); }); async function setupTest({ ...args } = {}) { const { cleanup: baseCleanup, ...ctx } = await NimbusTestUtils.setupTest({ ...args, clearTelemetry: true, }); return { ...ctx, async cleanup() { removePrefObservers(ctx.manager); assertNoObservers(ctx.manager); await baseCleanup(); }, }; } add_task(async function test_enroll_setPref_rolloutsAndExperiments() { /** * 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, }) { const { manager, cleanup } = await setupTest(); 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 NimbusTestUtils.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++; } await cleanup(); 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, ], }); } }); 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. let storePath; { const manager = NimbusTestUtils.stubs.manager(); await manager.store.init(); await manager.onStartup(); await NimbusTestUtils.assert.storeIsEmpty(manager.store); for (const [enrollmentKind, config] of Object.entries(configs)) { await NimbusTestUtils.enrollWithFeatureConfig(config, { manager, isRollout: enrollmentKind === ROLLOUT, }); } storePath = await NimbusTestUtils.saveStore(manager.store); removePrefObservers(manager); assertNoObservers(manager); // 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); } } // 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, manager, initExperimentAPI, cleanup } = await setupTest({ init: false, storePath, }); const setPrefSpy = sandbox.spy(PrefUtils, "setPref"); await initExperimentAPI(); 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]: manager.store.getRolloutForFeature(featureId)?.slug, [EXPERIMENT]: manager.store.getExperimentForFeature(featureId)?.slug, }; let i = 1; for (const enrollmentKind of unenrollOrder) { await manager.unenroll(slugs[enrollmentKind]); assertExpectedPrefValues( pref, branch, expectedValues[i], visibleValues[i], `after ${enrollmentKind} unenrollment` ); i++; } await cleanup(); Services.prefs.deleteBranch(pref); } { 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() { const LEGACY_FILTER = { category: "normandy", method: "unenroll", object: "nimbus_experiment", }; /** * 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, }) { Services.fog.testResetFOG(); Services.telemetry.snapshotEvents( Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, /* clear = */ true ); const { manager, cleanup } = await setupTest(); const cleanupFunctions = {}; const slugs = {}; 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; cleanupFunctions[enrollmentKind] = await NimbusTestUtils.enrollWithFeatureConfig(config, { manager, isRollout, }); const enrollments = isRollout ? manager.store.getAllActiveRollouts() : manager.store.getAllActiveExperiments(); 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 }); await NimbusTestUtils.waitForActiveEnrollments( expectedEnrollments.map(kind => slugs[kind]) ); 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 = manager.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]; await NimbusTestUtils.waitForInactiveEnrollment(slug); const enrollment = manager.store.get(slug); Assert.ok( enrollment !== null, `An enrollment of kind ${enrollmentKind} should exist` ); Assert.ok(!enrollment.active, "It should not be active"); Assert.equal( enrollment.unenrollReason, "changed-pref", "The unenrollment reason should be changed-pref" ); } } const gleanEvents = Glean.nimbusEvents.unenrollment.testGetValue("events"); const expectedLegacyEvents = Object.keys(configs) .filter(enrollmentKind => !expectedEnrollments.includes(enrollmentKind)) .map(enrollmentKind => ({ value: slugs[enrollmentKind], extra: { reason: "changed-pref", changedPref: pref, }, })); TelemetryTestUtils.assertEvents(expectedLegacyEvents, LEGACY_FILTER); if (expectedLegacyEvents.length) { const processedGleanEvents = gleanEvents.map(event => ({ reason: event.extra.reason, experiment: event.extra.experiment, changed_pref: event.extra.changed_pref, })); const expectedGleanEvents = expectedLegacyEvents.map(event => ({ experiment: event.value, reason: event.extra.reason, changed_pref: event.extra.changedPref, })); Assert.deepEqual( processedGleanEvents, expectedGleanEvents, "Glean should have the expected unenrollment events" ); } else { Assert.equal( gleanEvents, undefined, "Glean should have no unenrollment events" ); } const expectedEnrollmentStatusEvents = []; for (const enrollmentKind of Object.keys(configs)) { expectedEnrollmentStatusEvents.push({ slug: slugs[enrollmentKind], branch: "control", status: "Enrolled", reason: "Qualified", }); } for (const ev of expectedLegacyEvents) { expectedEnrollmentStatusEvents.push({ slug: ev.value, branch: "control", status: "Disqualified", reason: "ChangedPref", }); } const enrollmentStatusEvents = ( Glean.nimbusEvents.enrollmentStatus.testGetValue("events") ?? [] ).map(ev => ev.extra); for (const expectedEvent of expectedEnrollmentStatusEvents) { assertIncludes( enrollmentStatusEvents, expectedEvent, "Event should appear in the enrollment status telemetry" ); } Assert.equal( enrollmentStatusEvents.length, expectedEnrollmentStatusEvents.length, "We should see the expected number of enrollment status events" ); for (const enrollmentKind of expectedEnrollments) { await cleanupFunctions[enrollmentKind](); } Services.prefs.deleteBranch(pref); await cleanup(); } { 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 { manager, cleanup } = await setupTest(); const cleanupFunctions = []; cleanupFunctions.push( await NimbusTestUtils.enrollWithFeatureConfig(CONFIGS[USER][EXPERIMENT], { manager, }), await NimbusTestUtils.enrollWithFeatureConfig(CONFIGS[USER][ROLLOUT], { manager, isRollout: true, }), await NimbusTestUtils.enrollWithFeatureConfig( CONFIGS[DEFAULT][EXPERIMENT], { manager } ), await NimbusTestUtils.enrollWithFeatureConfig(CONFIGS[DEFAULT][ROLLOUT], { manager, isRollout: true, }) ); for (const cleanupFn of cleanupFunctions) { await cleanupFn(); } Services.prefs.deleteBranch(PREFS[USER]); Services.prefs.deleteBranch(PREFS[DEFAULT]); await cleanup(); }); 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 { manager, cleanup } = await setupTest(); const cleanupFns = []; const slugs = {}; setPrefs(pref, { defaultBranchValue, userBranchValue }); for (const [enrollmentKind, config] of Object.entries(configs)) { const isRollout = enrollmentKind === ROLLOUT; cleanupFns.push( await NimbusTestUtils.enrollWithFeatureConfig(config, { manager, isRollout, }) ); const enrollments = isRollout ? manager.store.getAllActiveRollouts() : manager.store.getAllActiveExperiments(); 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]; if (!expectedEnrolled) { await NimbusTestUtils.waitForInactiveEnrollment(slug); } const enrollment = manager.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(cleanupFns)) { await cleanupFn(); } } Services.prefs.deleteBranch(pref); await cleanup(); } { 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(prefBranch) { if (![USER, DEFAULT].includes(prefBranch)) { Assert.ok(false, `invalid branch ${prefBranch}`); } return new ExperimentFeature(featureId, { description: "Test feature that sets a pref", owner: "test@test.test", hasExposure: false, variables: { baz: { type: "string", description: "Test variable", setPref: { branch: prefBranch, pref, }, }, qux: { type: "string", description: "Test variable", }, }, }); } const config = { featureId, value: { qux: "qux", }, }; for (const prefBranch of [USER, DEFAULT]) { const feature = featureFactory(prefBranch); const cleanupFeature = NimbusTestUtils.addTestFeatures(feature); 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]) { const { manager, cleanup } = await setupTest(); setPrefs(pref, { defaultBranchValue, userBranchValue }); const doEnrollmentCleanup = await NimbusTestUtils.enrollWithFeatureConfig(config, { manager, isRollout, }); PrefUtils.setPref(pref, OVERWRITE_VALUE, { branch }); const enrollments = await manager.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" ); } await doEnrollmentCleanup(); await cleanup(); Services.prefs.deleteBranch(pref); } } } } cleanupFeature(); } }); add_task(async function test_restorePrefs_manifestChanged() { 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(prefBranch) { if (![USER, DEFAULT].includes(prefBranch)) { Assert.ok(false, `invalid branch ${prefBranch}`); } return new ExperimentFeature(featureId, { description: "Test feature that sets a pref on the default branch.", owner: "test@test.test", hasExposure: false, variables: { baz: { type: "string", description: "Test variable", setPref: { branch: prefBranch, 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, }) { const feature = featureFactory(branch); const cleanupFeatures = NimbusTestUtils.addTestFeatures(feature); setPrefs(pref, { defaultBranchValue, userBranchValue }); const slugs = {}; let userPref = null; // Enroll in some experiments and save the state to disk. let storePath; { const manager = NimbusTestUtils.stubs.manager(); await manager.store.init(); await manager.onStartup(); await NimbusTestUtils.assert.storeIsEmpty(manager.store); for (const [enrollmentKind, config] of Object.entries(configs)) { const isRollout = enrollmentKind === ROLLOUT; await NimbusTestUtils.enrollWithFeatureConfig(config, { manager, isRollout, }); const enrollments = isRollout ? manager.store.getAllActiveRollouts() : manager.store.getAllActiveExperiments(); Assert.equal( enrollments.length, 1, `Expected one ${enrollmentKind} enrollment` ); slugs[enrollmentKind] = enrollments[0].slug; } // 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 }); } storePath = await NimbusTestUtils.saveStore(manager.store); removePrefObservers(manager); assertNoObservers(manager); Services.prefs.deleteBranch(pref); } // 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.pref = BOGUS_PREF; break; default: Assert.ok(false, "invalid operation"); } const { manager, cleanup } = await setupTest({ storePath }); for (const enrollmentKind of expectedEnrollments) { const enrollment = manager.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 = manager.store.get(slug); Assert.ok( enrollment !== null, `An enrollment of kind ${enrollmentKind} should exist` ); Assert.ok(!enrollment.active, "It should not be active"); } } const gleanEvents = Glean.nimbusEvents.unenrollment.testGetValue("events"); 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]; await manager.unenroll(slug); } await cleanup(); 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, }); } } } } } } }); add_task(async function test_nested_prefs_enroll_both() { // See bugs 1850127 and 1850120. const feature = new ExperimentFeature("test-set-pref-nested", { description: "Nested prefs", owner: "test@test.test", hasExposure: false, variables: { enabled: { type: "boolean", description: "enable this feature", setPref: { branch: "default", pref: "nimbus.test-only.nested", }, }, setting: { type: "string", description: "a nested setting", setPref: { branch: "default", pref: "nimbus.test-only.nested.setting", }, }, }, }); const cleanupFeature = NimbusTestUtils.addTestFeatures(feature); async function doTest(enrollmentOrder) { PrefUtils.setPref("nimbus.test-only.nested", false, { branch: DEFAULT }); PrefUtils.setPref("nimbus.test-only.nested.setting", "default", { branch: DEFAULT, }); const rollout = NimbusTestUtils.factories.recipe.withFeatureConfig( "nested-rollout", { featureId: feature.featureId, value: { enabled: true }, }, { isRollout: true } ); const experiment = NimbusTestUtils.factories.recipe.withFeatureConfig( "nested-experiment", { featureId: feature.featureId, value: { setting: "custom" }, } ); const { manager, cleanup } = await setupTest(); const recipes = { [ROLLOUT]: rollout, [EXPERIMENT]: experiment, }; for (const kind of enrollmentOrder) { await manager.enroll(recipes[kind], "test"); } { const enrollments = manager.store .getAll() .filter(e => e.active) .map(e => e.slug); Assert.deepEqual( enrollments.sort(), [experiment.slug, rollout.slug].sort(), "Experiment and rollout should be enrolled" ); } assertExpectedPrefValues( "nimbus.test-only.nested", DEFAULT, true, true, "after enrollment" ); assertExpectedPrefValues( "nimbus.test-only.nested.setting", DEFAULT, "custom", "custom", "after enrollment" ); await manager.unenroll(experiment.slug); { const enrollments = manager.store .getAll() .filter(e => e.active) .map(e => e.slug); Assert.deepEqual( enrollments.sort(), [rollout.slug].sort(), "Rollout should still be enrolled" ); } assertExpectedPrefValues( "nimbus.test-only.nested", DEFAULT, true, true, "After experiment unenrollment" ); assertExpectedPrefValues( "nimbus.test-only.nested.setting", DEFAULT, "default", "default", "After experiment unenrollment" ); await manager.unenroll(rollout.slug); await cleanup(); } info( "Test we can enroll in both a rollout and experiment for a feature with nested pref setting" ); await doTest([ROLLOUT, EXPERIMENT]); info( "Test we can unenroll from just an experiment for a feature with nested pref setting" ); await doTest([EXPERIMENT, ROLLOUT]); cleanupFeature(); PrefUtils.setPref("nimbus.test-only.nested", null, { branch: DEFAULT }); PrefUtils.setPref("nimbus.test-only.nested.setting", null, { branch: DEFAULT, }); }); const TYPED_FEATURE = new ExperimentFeature("test-typed-prefs", { description: "Test feature that sets each type of pref", owner: "test@test.test", hasExposure: false, variables: { string: { type: "string", description: "test string variable", setPref: { branch: "default", pref: "nimbus.test-only.types.string", }, }, int: { type: "int", description: "test int variable", setPref: { branch: "default", pref: "nimbus.test-only.types.int", }, }, boolean: { type: "boolean", description: "test boolean variable", setPref: { branch: "default", pref: "nimbus.test-only.types.boolean", }, }, json: { type: "json", description: "test json variable", setPref: { branch: "default", pref: "nimbus.test-only.types.json", }, }, }, }); add_task(async function test_setPref_types() { const featureCleanup = NimbusTestUtils.addTestFeatures(TYPED_FEATURE); const { manager, cleanup } = await setupTest(); const json = { foo: "foo", bar: 12345, baz: true, qux: null, quux: ["corge"], }; const experimentCleanup = await NimbusTestUtils.enrollWithFeatureConfig( { featureId: TYPED_FEATURE.featureId, value: { string: "hello, world", int: 12345, boolean: true, json, }, }, { manager } ); const defaultBranch = Services.prefs.getDefaultBranch(null); Assert.equal( defaultBranch.getPrefType("nimbus.test-only.types.string"), Services.prefs.PREF_STRING ); Assert.equal( defaultBranch.getStringPref("nimbus.test-only.types.string"), "hello, world" ); Assert.equal( defaultBranch.getPrefType("nimbus.test-only.types.int"), Services.prefs.PREF_INT ); Assert.equal(defaultBranch.getIntPref("nimbus.test-only.types.int"), 12345); Assert.equal( defaultBranch.getPrefType("nimbus.test-only.types.boolean"), Services.prefs.PREF_BOOL ); Assert.equal( defaultBranch.getBoolPref("nimbus.test-only.types.boolean"), true ); Assert.equal( defaultBranch.getPrefType("nimbus.test-only.types.json"), Services.prefs.PREF_STRING ); const jsonPrefValue = JSON.parse( defaultBranch.getStringPref("nimbus.test-only.types.json") ); Assert.deepEqual(json, jsonPrefValue); await experimentCleanup(); featureCleanup(); await cleanup(); }); add_task(async function test_setPref_types_restore() { const featureCleanup = NimbusTestUtils.addTestFeatures(TYPED_FEATURE); const json = { foo: "foo", bar: 12345, baz: true, qux: null, quux: ["corge"], }; let storePath; { const manager = NimbusTestUtils.stubs.manager(); await manager.store.init(); await manager.onStartup(); await NimbusTestUtils.assert.storeIsEmpty(manager.store); await NimbusTestUtils.enrollWithFeatureConfig( { featureId: TYPED_FEATURE.featureId, value: { string: "hello, world", int: 12345, boolean: true, json, }, }, { manager } ); storePath = await NimbusTestUtils.saveStore(manager.store); removePrefObservers(manager); assertNoObservers(manager); for (const varDef of Object.values(TYPED_FEATURE.manifest.variables)) { Services.prefs.deleteBranch(varDef.setPref.pref); } } const { manager, cleanup } = await setupTest({ storePath }); const defaultBranch = Services.prefs.getDefaultBranch(null); Assert.equal( defaultBranch.getPrefType("nimbus.test-only.types.string"), Services.prefs.PREF_STRING ); Assert.equal( defaultBranch.getStringPref("nimbus.test-only.types.string"), "hello, world" ); Assert.equal( defaultBranch.getPrefType("nimbus.test-only.types.int"), Services.prefs.PREF_INT ); Assert.equal(defaultBranch.getIntPref("nimbus.test-only.types.int"), 12345); Assert.equal( defaultBranch.getPrefType("nimbus.test-only.types.boolean"), Services.prefs.PREF_BOOL ); Assert.equal( defaultBranch.getBoolPref("nimbus.test-only.types.boolean"), true ); Assert.equal( defaultBranch.getPrefType("nimbus.test-only.types.json"), Services.prefs.PREF_STRING ); const jsonPrefValue = JSON.parse( defaultBranch.getStringPref("nimbus.test-only.types.json") ); Assert.deepEqual(json, jsonPrefValue); const enrollment = manager.store.getExperimentForFeature( TYPED_FEATURE.featureId ); await manager.unenroll(enrollment.slug); await cleanup(); featureCleanup(); }); add_task(async function testDb() { const { manager, cleanup } = await setupTest(); PrefUtils.setPref("nimbus.qa.pref-1", "foo", { branch: DEFAULT }); await manager.enroll( NimbusTestUtils.factories.recipe.withFeatureConfig("slug", { featureId: "nimbus-qa-1", value: { value: "hello" }, }), "test" ); const conn = await ProfilesDatastoreService.getConnection(); const [result] = await conn.execute( ` SELECT json(setPrefs) as setPrefs FROM NimbusEnrollments WHERE profileId = :profileId AND slug = :slug; `, { slug: "slug", profileId: ExperimentAPI.profileId, } ); const setPrefs = JSON.parse(result.getResultByName("setPrefs")); const enrollment = manager.store.get("slug"); Assert.deepEqual( setPrefs, enrollment.prefs, "setPrefs stored in the database" ); Assert.deepEqual(setPrefs, [ { name: "nimbus.qa.pref-1", branch: "default", featureId: "nimbus-qa-1", variable: "value", originalValue: "foo", }, ]); await manager.unenroll("slug"); await cleanup(); Services.prefs.deleteBranch("nimbus.qa.pref-1"); });