/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ const { PrefUtils } = ChromeUtils.importESModule( "resource://normandy/lib/PrefUtils.sys.mjs" ); const { JsonSchema } = ChromeUtils.importESModule( "resource://gre/modules/JsonSchema.sys.mjs" ); const { ProfilesDatastoreService } = ChromeUtils.importESModule( "moz-src:///toolkit/profile/ProfilesDatastoreService.sys.mjs" ); const USER = "user"; const DEFAULT = "default"; const STRING_PREF = "test.nimbus.prefFlips.string"; const INT_PREF = "test.nimbus.prefFlips.int"; const BOOL_PREF = "test.nimbus.prefFlips.boolean"; const FEATURE_ID = "prefFlips"; const SET_BEFORE_VALUE = "set-before-value"; const USER_VALUE = "user-value"; const DEFAULT_VALUE = "default-value"; const PREF_FEATURES = { [USER]: new ExperimentFeature("test-set-pref-user-1", { description: "Test feature that sets prefs on the user branch via setPref", owner: "test@test.test", hasExposure: false, variables: { foo: { type: "string", description: "test variable", setPref: { branch: USER, pref: "nimbus.test-only.foo", }, }, }, }), [DEFAULT]: new ExperimentFeature("test-set-pref-default-1", { description: "Test feature that sets prefs on the default branch via setPref", owner: "test@test.test", hasExposure: false, variables: { foo: { type: "string", description: "test variable", setPref: { branch: DEFAULT, pref: "nimbus.test-only.foo", }, }, }, }), }; function setPrefs(prefs) { for (const [name, { userBranchValue, defaultBranchValue }] of Object.entries( prefs )) { // If the different prefs have the same value, we must set the user branch // value first. Otherwise when we try to set the user branch value after // the default value, it will see the value already set for the user // branch (because it falls back to the default branch value) and will not // set it, leaving only a default branch pref. if (typeof userBranchValue !== "undefined") { PrefUtils.setPref(name, userBranchValue); } if (typeof defaultBranchValue !== "undefined") { PrefUtils.setPref(name, defaultBranchValue, { branch: DEFAULT }); } } } function cleanupPrefs(prefs) { for (const name of Object.keys(prefs)) { Services.prefs.deleteBranch(name); } } function checkExpectedPrefs(prefs) { for (const [name, value] of Object.entries(prefs)) { Assert.equal( PrefUtils.getPref(name), value, `Pref ${name} has correct value` ); } } function checkExpectedPrefBranches(prefs) { for (const [ name, { defaultBranchValue = null, userBranchValue = null }, ] of Object.entries(prefs)) { if (userBranchValue === null) { Assert.ok( !Services.prefs.prefHasUserValue(name), `Pref ${name} has no value on user branch` ); } else { Assert.equal( PrefUtils.getPref(name, { branch: USER }), userBranchValue, `Pref ${name} has correct value on user branch` ); } if (defaultBranchValue === null) { Assert.ok( !Services.prefs.prefHasDefaultValue(name), `Pref ${name} has no value on default branch` ); } else { Assert.equal( PrefUtils.getPref(name, { branch: DEFAULT }), defaultBranchValue, `Pref ${name} has correct value on default branch` ); } } } add_setup(function setup() { Services.fog.initializeFOG(); Services.telemetry.clearEvents(); registerCleanupFunction( NimbusTestUtils.addTestFeatures(PREF_FEATURES[USER], PREF_FEATURES[DEFAULT]) ); }); async function setupTest({ ...args } = {}) { const { cleanup: baseCleanup, ...ctx } = await NimbusTestUtils.setupTest({ ...args, clearTelemetry: true, }); return { ...ctx, async cleanup() { assertNoObservers(ctx.manager); await NimbusTestUtils.waitForAllUnenrollments(); await baseCleanup(); }, }; } add_task(async function test_schema() { const schema = await fetch( "resource://nimbus/schemas/PrefFlipsFeature.schema.json" ).then(rsp => rsp.json()); const validator = new JsonSchema.Validator(schema); const ALLOWED_TEST_CASES = [ { prefs: {} }, { prefs: { "foo.string": { branch: USER, value: "value", }, "foo.int": { branch: USER, value: 123, }, "foo.bool": { branch: USER, value: true, }, "bar.string": { branch: DEFAULT, value: "value", }, "bar.int": { branch: DEFAULT, value: 345, }, "bar.bool": { branch: DEFAULT, value: false, }, }, }, ]; for (const obj of ALLOWED_TEST_CASES) { const result = validator.validate(obj); Assert.ok( result.valid, `validated: ${JSON.stringify(result.errors, null, 2)}` ); } const DISALLOWED_TEST_CASES = [ {}, { prefs: { "foo.bar.baz": { branch: "other", value: "value", }, }, }, { prefs: { "foo.bar.baz": {}, }, }, { prefs: { "foo.bar.baz": { branch: USER, }, }, }, { prefs: { "foo.bar.baz": { branch: DEFAULT, }, }, }, { prefs: { "foo.bar.baz": { value: "value", }, }, }, { prefs: { "foo.bar.baz": { branch: DEFAULT, value: null, }, }, }, ]; for (const obj of DISALLOWED_TEST_CASES) { const result = validator.validate(obj); Assert.ok(!result.valid); } }); add_task(async function test_prefFlips() { const setUserPrefs = { prefs: { [STRING_PREF]: { branch: USER, value: "hello, world", }, [INT_PREF]: { branch: USER, value: 123, }, [BOOL_PREF]: { branch: USER, value: true, }, }, }; const setDefaultPrefs = { prefs: { [STRING_PREF]: { branch: DEFAULT, value: "hello, world", }, [INT_PREF]: { branch: DEFAULT, value: 123, }, [BOOL_PREF]: { branch: DEFAULT, value: true, }, }, }; const clearUserPrefs = { prefs: { [STRING_PREF]: { branch: USER, value: null, }, [INT_PREF]: { branch: USER, value: null, }, [BOOL_PREF]: { branch: USER, value: null, }, }, }; const PRE_SET_PREFS = { [USER]: { [STRING_PREF]: { userBranchValue: "goodbye, world" }, [INT_PREF]: { userBranchValue: 234 }, [BOOL_PREF]: { userBranchValue: false }, }, [DEFAULT]: { [STRING_PREF]: { defaultBranchValue: "goodbye, world" }, [INT_PREF]: { defaultBranchValue: 234 }, [BOOL_PREF]: { defaultBranchValue: false }, }, BOTH_BRANCHES: { [STRING_PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: DEFAULT_VALUE, }, [INT_PREF]: { userBranchValue: 2, defaultBranchValue: 3 }, [BOOL_PREF]: { userBranchValue: false, defaultBranchValue: false }, }, }; const TEST_CASES = [ { name: "Set prefs on the user branch", featureValue: setUserPrefs, }, { name: "Set prefs on the user branch with pre-existing values on the user branch", featureValue: setUserPrefs, setPrefsBefore: PRE_SET_PREFS[USER], }, { name: "Set prefs on the user branch with pre-existing values on the default branch", featureValue: setUserPrefs, setPrefsBefore: PRE_SET_PREFS[DEFAULT], }, { name: "Set prefs on the user branch with pre-existing values on both branches", featureValue: setUserPrefs, setPrefsBefore: PRE_SET_PREFS.BOTH_BRANCHES, }, { name: "Set prefs on the default branch", featureValue: setDefaultPrefs, }, { name: "Set prefs on the default branch with pre-existing values on the default branch", featureValue: setDefaultPrefs, setPrefsBefore: PRE_SET_PREFS[DEFAULT], }, { name: "Set prefs on the default branch with pre-existing values on the user branch", featureValue: setDefaultPrefs, setPrefsBefore: PRE_SET_PREFS[USER], expectedPrefs: { [STRING_PREF]: PRE_SET_PREFS[USER][STRING_PREF].userBranchValue, [INT_PREF]: PRE_SET_PREFS[USER][INT_PREF].userBranchValue, [BOOL_PREF]: PRE_SET_PREFS[USER][BOOL_PREF].userBranchValue, }, }, { name: "Set prefs on the default branch with pre-existing values on both branches", featureValue: setDefaultPrefs, setPrefsBefore: PRE_SET_PREFS.BOTH_BRANCHES, expectedPrefs: { [STRING_PREF]: PRE_SET_PREFS.BOTH_BRANCHES[STRING_PREF].userBranchValue, [INT_PREF]: PRE_SET_PREFS.BOTH_BRANCHES[INT_PREF].userBranchValue, [BOOL_PREF]: PRE_SET_PREFS.BOTH_BRANCHES[BOOL_PREF].userBranchValue, }, }, { name: "Clearing prefs on the user branch (with value null) without pre-existing values", featureValue: clearUserPrefs, }, { name: "Clearing prefs on the user branch (with value null) with pre-existing values on the user branch", featureValue: clearUserPrefs, setPrefsBefore: PRE_SET_PREFS[USER], }, { name: "Clearing prefs on the user branch (with value null) with pre-existing values on the default branch", featureValue: clearUserPrefs, setPrefsBefore: PRE_SET_PREFS[DEFAULT], // This will not affect the default branch prefs. expectedPrefs: { [STRING_PREF]: PRE_SET_PREFS[DEFAULT][STRING_PREF].defaultBranchValue, [INT_PREF]: PRE_SET_PREFS[DEFAULT][INT_PREF].defaultBranchValue, [BOOL_PREF]: PRE_SET_PREFS[DEFAULT][BOOL_PREF].defaultBranchValue, }, }, { name: "Clearing prefs on the user branch (with value null) with pre-existing values on both branches", featureValue: clearUserPrefs, setPrefsBefore: PRE_SET_PREFS.BOTH_BRANCHES, expectedPrefs: { [STRING_PREF]: PRE_SET_PREFS.BOTH_BRANCHES[STRING_PREF].defaultBranchValue, [INT_PREF]: PRE_SET_PREFS.BOTH_BRANCHES[INT_PREF].defaultBranchValue, [BOOL_PREF]: PRE_SET_PREFS.BOTH_BRANCHES[BOOL_PREF].defaultBranchValue, }, }, ]; for (const [i, { name, ...testCase }] of TEST_CASES.entries()) { info(`Running test case ${i}: ${name}`); const { // The feature config to enroll. featureValue, // Prefs that should be set before enrollment. These will be undone after // each test case. setPrefsBefore = {}, // Additional prefs to check after enrollment. They will be checked on the // user branch. expectedPrefs = {}, } = testCase; const { manager, cleanup } = await setupTest(); info("Setting initial values of prefs..."); setPrefs(setPrefsBefore); // Collect the values of any prefs that will be set by the enrollment so we // can compare their values after unenrollment. const prefValuesBeforeEnrollment = Object.fromEntries( Object.keys(featureValue.prefs).map(prefName => [ prefName, PrefUtils.getPref(prefName), ]) ); info("Enrolling..."); const cleanupExperiment = await NimbusTestUtils.enrollWithFeatureConfig( { featureId: FEATURE_ID, value: featureValue, }, { manager, isRollout: true, } ); info("Checking prefs were set by enrollment..."); for (const [prefName, { branch, value }] of Object.entries( featureValue.prefs )) { if (typeof value === "undefined" || value === null) { if (branch === USER) { Assert.ok( !Services.prefs.prefHasUserValue(prefName), `${prefName} was cleared on the user branch` ); } else if (prefValuesBeforeEnrollment[prefName] !== null) { // Can't clear the user branch. Assert.equal( PrefUtils.getPref(prefName, { branch }), prefValuesBeforeEnrollment ); } else { Assert.equal(PrefUtils.getPref(prefName, { branch }), value); } } else { Assert.equal(PrefUtils.getPref(prefName, { branch }), value); } } if (expectedPrefs) { info("Checking expected prefs..."); checkExpectedPrefs(expectedPrefs); } info("Unenrolling..."); await cleanupExperiment(); info("Checking prefs were restored after unenrollment..."); // After unenrollment, the prefs should have been restored to their values // before enrollment. for (const [prefName, originalValue] of Object.entries( prefValuesBeforeEnrollment )) { // If the pref was set on the default branch, it won't be cleared. It will // persist until the next restart. const expectedValue = featureValue.prefs[prefName].branch === "default" && originalValue === null ? featureValue.prefs[prefName].value : originalValue; Assert.equal(PrefUtils.getPref(prefName), expectedValue); } info("Cleaning up..."); // Clear all the prefs we specified in `setPrefsBefore`. cleanupPrefs(setPrefsBefore); // Clear all prefs specified by the enrollment. for (const prefName of Object.keys(featureValue.prefs)) { Services.prefs.deleteBranch(prefName); } await cleanup(); } }); add_task(async function test_prefFlips_unenrollment() { const PREF_FOO = "nimbus.test-only.foo"; const PREF_BAR = "nimbus.test-only.bar"; const SLUG_1 = "slug-1"; const SLUG_2 = "slug-2"; const SLUG_3 = "slug-3"; const EXPERIMENT_VALUE = "experiment-value"; const TEST_CASES = [ // Single enrollment case (experiments) { name: "set pref on the user branch with a prefFlips experiment and change that pref on the user branch", enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER } }, }, }, ], setPrefsAfter: { [PREF_FOO]: { userBranchValue: USER_VALUE } }, expectedUnenrollments: [SLUG_1], expectedPrefs: { [PREF_FOO]: USER_VALUE }, }, { name: "set pref on the user branch with a prefFlips experiment and change that pref on the default branch", enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER } }, }, }, ], setPrefsAfter: { [PREF_FOO]: { defaultBranchValue: DEFAULT_VALUE } }, expectedEnrollments: [SLUG_1], expectedPrefs: { [PREF_FOO]: EXPERIMENT_VALUE }, }, { name: "set pref on the default branch with a prefFlips experiment and change that pref on the user branch", enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT } }, }, }, ], setPrefsAfter: { [PREF_FOO]: { userBranchValue: USER_VALUE } }, expectedUnenrollments: [SLUG_1], expectedPrefs: { [PREF_FOO]: USER_VALUE }, }, { name: "set pref on the default branch with a prefFlips experiment and change that pref on the default branch", enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT } }, }, }, ], setPrefsAfter: { [PREF_FOO]: { defaultBranchValue: DEFAULT_VALUE } }, expectedUnenrollments: [SLUG_1], expectedPrefs: { [PREF_FOO]: DEFAULT_VALUE }, }, // Single enrollment case, multiple prefs being reset { name: "set prefs on the user branch with a prefFlips experiment and change one pref on the user branch", setPrefsBefore: { [PREF_FOO]: { userBranchValue: SET_BEFORE_VALUE } }, enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER }, [PREF_BAR]: { value: EXPERIMENT_VALUE, branch: USER }, }, }, }, ], setPrefsAfter: { [PREF_BAR]: { userBranchValue: USER_VALUE } }, expectedUnenrollments: [SLUG_1], expectedPrefs: { [PREF_FOO]: SET_BEFORE_VALUE, [PREF_BAR]: USER_VALUE }, }, { name: "set prefs on the user branch with a prefFlips experiment and change one pref on the default branch", setPrefsBefore: { [PREF_FOO]: { userBranchValue: SET_BEFORE_VALUE } }, enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER }, [PREF_BAR]: { value: EXPERIMENT_VALUE, branch: USER }, }, }, }, ], setPrefsAfter: { [PREF_BAR]: { defaultBranchValue: DEFAULT_VALUE } }, expectedEnrollments: [SLUG_1], expectedPrefs: { [PREF_FOO]: EXPERIMENT_VALUE, [PREF_BAR]: EXPERIMENT_VALUE, }, }, { name: "set prefs on the default branch with a prefFlips experiment and change one pref on the user branch", setPrefsBefore: { [PREF_FOO]: { defaultBranchValue: SET_BEFORE_VALUE } }, enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT }, [PREF_BAR]: { value: EXPERIMENT_VALUE, branch: DEFAULT }, }, }, }, ], setPrefsAfter: { [PREF_BAR]: { userBranchValue: USER_VALUE } }, expectedUnenrollments: [SLUG_1], expectedPrefs: { [PREF_FOO]: SET_BEFORE_VALUE, [PREF_BAR]: USER_VALUE }, }, { name: "set prefs on the default branch with a prefFlips experiment and change one pref on the default branch", setPrefsBefore: { [PREF_FOO]: { defaultBranchValue: SET_BEFORE_VALUE } }, enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT }, [PREF_BAR]: { value: EXPERIMENT_VALUE, branch: DEFAULT }, }, }, }, ], setPrefsAfter: { [PREF_BAR]: { defaultBranchValue: DEFAULT_VALUE } }, expectedUnenrollments: [SLUG_1], expectedPrefs: { [PREF_FOO]: SET_BEFORE_VALUE, [PREF_BAR]: DEFAULT_VALUE, }, }, // Multiple enrollment cases // - test that we leave enrollments that do not conflict with the set pref { name: "set pref on the user branch with two prefFlips experiments and then change a pref controlled by only one experiment on the user branch", setPrefsBefore: { [PREF_FOO]: { userBranchValue: SET_BEFORE_VALUE } }, enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER } }, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER }, [PREF_BAR]: { value: EXPERIMENT_VALUE, branch: USER }, }, }, }, ], setPrefsAfter: { [PREF_BAR]: { userBranchValue: USER_VALUE } }, expectedEnrollments: [SLUG_1], expectedUnenrollments: [SLUG_2], expectedPrefs: { [PREF_FOO]: EXPERIMENT_VALUE, [PREF_BAR]: USER_VALUE }, }, { name: "set pref on the user branch two prefFlips experiments and then change a pref controlled by only one experiment on the default branch", setPrefsBefore: { [PREF_FOO]: { userBranchValue: SET_BEFORE_VALUE } }, enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER } }, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER }, [PREF_BAR]: { value: EXPERIMENT_VALUE, branch: USER }, }, }, }, ], setPrefsAfter: { [PREF_BAR]: { defaultBranchValue: DEFAULT_VALUE } }, expectedEnrollments: [SLUG_1, SLUG_2], expectedPrefs: { [PREF_FOO]: EXPERIMENT_VALUE, [PREF_BAR]: EXPERIMENT_VALUE, }, }, // - test that we unenroll from all conflicting experiments { name: "set pref on the default branch with two prefFlips experiments and then change that pref on the user branch", setPrefsBefore: { [PREF_FOO]: { defaultBranchValue: SET_BEFORE_VALUE } }, enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT } }, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT } }, }, }, ], setPrefsAfter: { [PREF_FOO]: { userBranchValue: USER_VALUE } }, expectedUnenrollments: [SLUG_1, SLUG_2], expectedPrefs: { [PREF_FOO]: USER_VALUE }, }, { name: "set pref on the default branch with two prefFlips experiments and then change that pref on the default branch", setPrefsBefore: { [PREF_FOO]: { defaultBranchValue: SET_BEFORE_VALUE } }, enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT } }, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT } }, }, }, ], setPrefsAfter: { [PREF_FOO]: { defaultBranchValue: DEFAULT_VALUE } }, expectedUnenrollments: [SLUG_1, SLUG_2], expectedPrefs: { [PREF_FOO]: DEFAULT_VALUE }, }, // - test we unenroll when the experiments conflict with eachother. { name: "set pref on the user branch with two experiments with conflicting values", enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: SLUG_1, branch: USER } }, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: SLUG_2, branch: USER } }, }, }, ], expectedEnrollments: [SLUG_1], expectedUnenrollments: [SLUG_2], expectedPrefs: { [PREF_FOO]: SLUG_1 }, }, { name: "set pref on the default branch with two experiments with conflicting values", enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: SLUG_1, branch: DEFAULT } }, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: SLUG_2, branch: DEFAULT } }, }, }, ], expectedEnrollments: [SLUG_1], expectedUnenrollments: [SLUG_2], expectedPrefs: { [PREF_FOO]: SLUG_1 }, }, { name: "set pref on the user branch with an experiment and set it on the default branch with another", enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER } }, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT } }, }, }, ], expectedEnrollments: [SLUG_1], expectedUnenrollments: [SLUG_2], expectedPrefs: { [PREF_FOO]: EXPERIMENT_VALUE }, }, { name: "set pref on the default branch with an experiment and set it on the user branch with another", enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT } }, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER } }, }, }, ], expectedEnrollments: [SLUG_1], expectedUnenrollments: [SLUG_2], expectedPrefs: { [PREF_FOO]: EXPERIMENT_VALUE }, }, // Multiple enrollment cases (prefFlips -> setPref) // NB: We don't need to test setPref experiments/rollouts on both branches // for the same pref because that configuration is prohibited by // gen_feature_manifests.py // * prefFlip experiments -> setPref experiment { name: "enroll in prefFlips experiments on the user branch and then a setPref experiment on the user branch", enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER } }, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER } }, }, }, { slug: SLUG_3, featureId: PREF_FEATURES[USER].featureId, value: { foo: EXPERIMENT_VALUE, }, }, ], expectedEnrollments: [SLUG_3], expectedUnenrollments: [SLUG_1, SLUG_2], expectedPrefs: { [PREF_FOO]: EXPERIMENT_VALUE }, }, { name: "enroll in prefFlips experiments on the default branch and then a setPref experiment on the user branch", enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT } }, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT } }, }, }, { slug: SLUG_3, featureId: PREF_FEATURES[USER].featureId, value: { foo: EXPERIMENT_VALUE, }, }, ], expectedEnrollments: [SLUG_3], expectedUnenrollments: [SLUG_1, SLUG_2], expectedPrefs: { [PREF_FOO]: EXPERIMENT_VALUE }, }, { name: "enroll in prefFlips experiments on the user branch and then a setPref experiment on the default branch", enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER } }, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER } }, }, }, { slug: SLUG_3, featureId: PREF_FEATURES[DEFAULT].featureId, value: { foo: EXPERIMENT_VALUE, }, }, ], expectedEnrollments: [SLUG_3], expectedUnenrollments: [SLUG_1, SLUG_2], expectedPrefs: { [PREF_FOO]: EXPERIMENT_VALUE }, }, { name: "enroll in prefFlips experiments on the default branch and then a setPref experiment on the default branch", enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT } }, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT } }, }, }, { slug: SLUG_3, featureId: PREF_FEATURES[DEFAULT].featureId, value: { foo: EXPERIMENT_VALUE, }, }, ], expectedEnrollments: [SLUG_3], expectedUnenrollments: [SLUG_1, SLUG_2], expectedPrefs: { [PREF_FOO]: EXPERIMENT_VALUE }, }, { name: "enroll in prefFlip experiment on the user branch and then a setPref experiment on the user branch and unenroll to check if original values are restored (no original value)", enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: SLUG_1, branch: USER }, }, }, }, { slug: SLUG_2, featureId: PREF_FEATURES[USER].featureId, value: { foo: SLUG_2, }, }, ], expectedEnrollments: [SLUG_2], expectedUnenrollments: [SLUG_1], unenrollmentOrder: [SLUG_2], expectedPrefs: { [PREF_FOO]: null, }, }, { name: "enroll in prefFlip experiment on the user branch and then a setPref experiment on the default branch and unenroll to check if original values are restored (no original value)", enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: SLUG_1, branch: USER }, }, }, }, { slug: SLUG_2, featureId: PREF_FEATURES[DEFAULT].featureId, value: { foo: SLUG_2, }, }, ], expectedEnrollments: [SLUG_2], expectedUnenrollments: [SLUG_1], unenrollmentOrder: [SLUG_2], expectedPrefs: { [PREF_FOO]: SLUG_2, // we can't clear the default branch }, }, { name: "enroll in prefFlip experiment on the default branch and then a setPref experiment on the user branch and unenroll to check if original values are restored (no original value)", enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: SLUG_1, branch: USER }, }, }, }, { slug: SLUG_2, featureId: PREF_FEATURES[USER].featureId, value: { foo: SLUG_2, }, }, ], expectedEnrollments: [SLUG_2], expectedUnenrollments: [SLUG_1], unenrollmentOrder: [SLUG_2], expectedPrefs: { [PREF_FOO]: null, }, }, { name: "enroll in prefFlip experiment on the default branch and then a setPref experiment on the default branch and unenroll to check if original values are restored (no original value)", enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: SLUG_1, branch: USER }, }, }, }, { slug: SLUG_2, featureId: PREF_FEATURES[DEFAULT].featureId, value: { foo: SLUG_2, }, }, ], expectedEnrollments: [SLUG_2], expectedUnenrollments: [SLUG_1], unenrollmentOrder: [SLUG_2], expectedPrefs: { [PREF_FOO]: SLUG_2, }, }, { name: "enroll in prefFlip experiment on the user branch and then a setPref experiment on the user branch and unenroll to check if original values are restored", setPrefsBefore: { [PREF_FOO]: { userBranchValue: SET_BEFORE_VALUE } }, enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: SLUG_1, branch: USER }, }, }, }, { slug: SLUG_2, featureId: PREF_FEATURES[USER].featureId, value: { foo: SLUG_2, }, }, ], expectedEnrollments: [SLUG_2], expectedUnenrollments: [SLUG_1], unenrollmentOrder: [SLUG_2], expectedPrefs: { [PREF_FOO]: SET_BEFORE_VALUE, }, }, { name: "enroll in prefFlip experiment on the user branch and then a setPref experiment on the default branch and unenroll to check if original values are restored", setPrefsBefore: { [PREF_FOO]: { defaultBranchValue: SET_BEFORE_VALUE } }, enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: SLUG_1, branch: USER }, }, }, }, { slug: SLUG_2, featureId: PREF_FEATURES[DEFAULT].featureId, value: { foo: SLUG_2, }, }, ], expectedEnrollments: [SLUG_2], expectedUnenrollments: [SLUG_1], unenrollmentOrder: [SLUG_2], expectedPrefs: { [PREF_FOO]: SET_BEFORE_VALUE, }, }, { name: "enroll in prefFlip experiment on the default branch and then a setPref experiment on the user branch and unenroll to check if original values are restored", setPrefsBefore: { [PREF_FOO]: { defaultBranchValue: SET_BEFORE_VALUE } }, enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: SLUG_1, branch: USER }, }, }, }, { slug: SLUG_2, featureId: PREF_FEATURES[USER].featureId, value: { foo: SLUG_2, }, }, ], expectedEnrollments: [SLUG_2], expectedUnenrollments: [SLUG_1], unenrollmentOrder: [SLUG_2], expectedPrefs: { [PREF_FOO]: SET_BEFORE_VALUE, }, }, { name: "enroll in prefFlip experiment on the default branch and then a setPref experiment on the default branch and unenroll to check if original values are restored", setPrefsBefore: { [PREF_FOO]: { defaultBranchValue: SET_BEFORE_VALUE } }, enrollmentOrder: [ { slug: SLUG_1, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: SLUG_1, branch: USER }, }, }, }, { slug: SLUG_2, featureId: PREF_FEATURES[DEFAULT].featureId, value: { foo: SLUG_2, }, }, ], expectedEnrollments: [SLUG_2], expectedUnenrollments: [SLUG_1], unenrollmentOrder: [SLUG_2], expectedPrefs: { [PREF_FOO]: SET_BEFORE_VALUE, }, }, // * setPref experiment, rollout -> prefFlips experiment { name: "enroll in a setPref experiment and rollout on the user branch then a prefFlips experiment on the user branch", enrollmentOrder: [ { slug: SLUG_1, featureId: PREF_FEATURES[USER].featureId, value: { foo: EXPERIMENT_VALUE, }, }, { slug: SLUG_2, isRollout: true, featureId: PREF_FEATURES[USER].featureId, value: { foo: EXPERIMENT_VALUE, }, }, { slug: SLUG_3, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER } }, }, }, ], expectedEnrollments: [SLUG_3], expectedUnenrollments: [SLUG_1, SLUG_2], expectedPrefs: { [PREF_FOO]: EXPERIMENT_VALUE }, }, { name: "enroll in a setPref experiment and rollout on the user branch then a prefFlips experiment on the default branch", enrollmentOrder: [ { slug: SLUG_1, featureId: PREF_FEATURES[USER].featureId, value: { foo: EXPERIMENT_VALUE, }, }, { slug: SLUG_2, isRollout: true, featureId: PREF_FEATURES[USER].featureId, value: { foo: EXPERIMENT_VALUE, }, }, { slug: SLUG_3, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT } }, }, }, ], expectedEnrollments: [SLUG_3], expectedUnenrollments: [SLUG_1, SLUG_2], expectedPrefs: { [PREF_FOO]: EXPERIMENT_VALUE }, }, { name: "enroll in a setPref experiment and rollout on the default branch then a prefFlips experiment on the user branch", enrollmentOrder: [ { slug: SLUG_1, featureId: PREF_FEATURES[DEFAULT].featureId, value: { foo: EXPERIMENT_VALUE, }, }, { slug: SLUG_2, isRollout: true, featureId: PREF_FEATURES[DEFAULT].featureId, value: { foo: EXPERIMENT_VALUE, }, }, { slug: SLUG_3, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER } }, }, }, ], expectedEnrollments: [SLUG_3], expectedUnenrollments: [SLUG_1, SLUG_2], expectedPrefs: { [PREF_FOO]: EXPERIMENT_VALUE }, }, { name: "enroll in a setPref experiment and rollout on the default branch then a prefFlips experiment on the default branch", enrollmentOrder: [ { slug: SLUG_1, featureId: PREF_FEATURES[DEFAULT].featureId, value: { foo: EXPERIMENT_VALUE, }, }, { slug: SLUG_2, isRollout: true, featureId: PREF_FEATURES[DEFAULT].featureId, value: { foo: EXPERIMENT_VALUE, }, }, { slug: SLUG_3, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT } }, }, }, ], expectedEnrollments: [SLUG_3], expectedUnenrollments: [SLUG_1, SLUG_2], expectedPrefs: { [PREF_FOO]: EXPERIMENT_VALUE }, }, { name: "enroll in a setPref experiment on the user branch and a prefFlips experiment on the user branch and unenroll to check if original values are restored (no original value)", enrollmentOrder: [ { slug: SLUG_1, featureId: PREF_FEATURES[USER].featureId, value: { foo: SLUG_1, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: SLUG_2, branch: USER }, }, }, }, ], expectedEnrollments: [SLUG_2], expectedUnenrollments: [SLUG_1], unenrollmentOrder: [SLUG_2], expectedPrefs: { [PREF_FOO]: null, // we can't clear the default branch }, }, { name: "enroll in a setPref experiment on the user branch and a prefFlips experiment on the default branch and unenroll to check if original values are restored (no original value)", enrollmentOrder: [ { slug: SLUG_1, featureId: PREF_FEATURES[USER].featureId, value: { foo: SLUG_1, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: SLUG_2, branch: DEFAULT }, }, }, }, ], expectedEnrollments: [SLUG_2], expectedUnenrollments: [SLUG_1], unenrollmentOrder: [SLUG_2], expectedPrefs: { [PREF_FOO]: SLUG_2, // Cannot clear the default branch }, }, { name: "enroll in a setPref experiment on the default branch and a prefFlips experiment on the user branch and unenroll to check if original values are restored (no original value)", enrollmentOrder: [ { slug: SLUG_1, featureId: PREF_FEATURES[DEFAULT].featureId, value: { foo: SLUG_1, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: SLUG_2, branch: USER }, }, }, }, ], expectedEnrollments: [SLUG_2], expectedUnenrollments: [SLUG_1], unenrollmentOrder: [SLUG_2], expectedPrefs: { [PREF_FOO]: SLUG_1, // cannot clear the default branch }, }, { name: "enroll in a setPref experiment on the default branch and a prefFlips experiment on the default branch and unenroll to check if original values are restored (no original value)", enrollmentOrder: [ { slug: SLUG_1, featureId: PREF_FEATURES[DEFAULT].featureId, value: { foo: SLUG_1, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: SLUG_2, branch: DEFAULT }, }, }, }, ], expectedEnrollments: [SLUG_2], expectedUnenrollments: [SLUG_1], unenrollmentOrder: [SLUG_2], expectedPrefs: { [PREF_FOO]: SLUG_2, // cannot clear the default branch }, }, { name: "enroll in a setPref experiment on the user branch and a prefFlips experiment on the user branch and unenroll to check if original values are restored", setPrefsBefore: { [PREF_FOO]: { userBranchValue: SET_BEFORE_VALUE }, }, enrollmentOrder: [ { slug: SLUG_1, featureId: PREF_FEATURES[USER].featureId, value: { foo: EXPERIMENT_VALUE, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER }, }, }, }, ], expectedEnrollments: [SLUG_2], expectedUnenrollments: [SLUG_1], unenrollmentOrder: [SLUG_2], expectedPrefs: { [PREF_FOO]: SET_BEFORE_VALUE, }, }, { name: "enroll in a setPref experiment on the user branch and a prefFlips experiment on the default branch and unenroll to check if original values are restored", setPrefsBefore: { [PREF_FOO]: { userBranchValue: SET_BEFORE_VALUE }, }, enrollmentOrder: [ { slug: SLUG_1, featureId: PREF_FEATURES[USER].featureId, value: { foo: EXPERIMENT_VALUE, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT }, }, }, }, ], expectedEnrollments: [SLUG_2], expectedUnenrollments: [SLUG_1], unenrollmentOrder: [SLUG_2], expectedPrefs: { [PREF_FOO]: SET_BEFORE_VALUE, }, }, { name: "enroll in a setPref experiment on the default branch and a prefFlips experiment on the user branch and unenroll to check if original values are restored", setPrefsBefore: { [PREF_FOO]: { defaultBranchValue: SET_BEFORE_VALUE }, }, enrollmentOrder: [ { slug: SLUG_1, featureId: PREF_FEATURES[DEFAULT].featureId, value: { foo: EXPERIMENT_VALUE, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: USER }, }, }, }, ], expectedEnrollments: [SLUG_2], expectedUnenrollments: [SLUG_1], unenrollmentOrder: [SLUG_2], expectedPrefs: { [PREF_FOO]: SET_BEFORE_VALUE, }, }, { name: "enroll in a setPref experiment on the default branch and a prefFlips experiment on the default branch and unenroll to check if original values are restored", setPrefsBefore: { [PREF_FOO]: { defaultBranchValue: SET_BEFORE_VALUE }, }, enrollmentOrder: [ { slug: SLUG_1, featureId: PREF_FEATURES[DEFAULT].featureId, value: { foo: EXPERIMENT_VALUE, }, }, { slug: SLUG_2, featureId: FEATURE_ID, value: { prefs: { [PREF_FOO]: { value: EXPERIMENT_VALUE, branch: DEFAULT }, }, }, }, ], expectedEnrollments: [SLUG_2], expectedUnenrollments: [SLUG_1], unenrollmentOrder: [SLUG_2], expectedPrefs: { [PREF_FOO]: SET_BEFORE_VALUE, }, }, ]; for (const [i, { name, ...testCase }] of TEST_CASES.entries()) { info(`Running test case ${i}: ${name}`); const { // Prefs that should be set after enrollment. These will be undone after // each test case. setPrefsBefore = {}, // The slugs to enroll in the order they should be enrolled in, and // whether or not they should enroll as rollouts. enrollmentOrder, // Prefs that should be set after enrollment. These will be undone // after each test case. setPrefsAfter = {}, // The expected active enrollments after all enrollments have finished. expectedEnrollments = [], // The expected inactive enrollments after all enrollments have finished. expectedUnenrollments = [], // The slugs to unenroll from after enrolling and settings prefs but // before checking pref values. unenrollmentOrder, // Prefs to check after enrollment. They will be checked on the user // branch. expectedPrefs, } = testCase; info("Setting prefs before enrollment..."); setPrefs(setPrefsBefore); const { manager, cleanup } = await setupTest(); info("Enrolling..."); for (const { slug, isRollout = false, ...featureConfig } of enrollmentOrder) { await NimbusTestUtils.enrollWithFeatureConfig(featureConfig, { slug, manager, isRollout, }); } info("Setting prefs after enrollment..."); setPrefs(setPrefsAfter); info("Checking expected enrollments..."); for (const slug of expectedEnrollments) { const enrollment = manager.store.get(slug); Assert.ok( enrollment !== null && typeof enrollment !== "undefined", `An enrollment for ${slug} should exist` ); Assert.ok(enrollment.active, `It should still be active`); } await NimbusTestUtils.waitForActiveEnrollments(expectedEnrollments); info("Checking expected unenrollments..."); for (const slug of expectedUnenrollments) { const enrollment = manager.store.get(slug); Assert.ok(!!enrollment, `An enrollment for ${slug} should exist`); Assert.ok(!enrollment.active, "It should no longer be active"); } let expectedCurrentEnrollments = new Set(expectedEnrollments).difference( new Set(expectedUnenrollments) ); await NimbusTestUtils.waitForActiveEnrollments( Array.from(expectedCurrentEnrollments) ); if (unenrollmentOrder) { info("Unenrolling from specific experiments before checking prefs..."); for (const slug of unenrollmentOrder ?? []) { await manager.unenroll(slug); } } expectedCurrentEnrollments = expectedCurrentEnrollments.difference( new Set(unenrollmentOrder) ); await NimbusTestUtils.waitForActiveEnrollments( Array.from(expectedCurrentEnrollments) ); if (expectedPrefs) { info("Checking expected prefs..."); checkExpectedPrefs(expectedPrefs); } info("Unenrolling from active experiments..."); for (const slug of expectedEnrollments) { if (!(unenrollmentOrder ?? []).includes(slug)) { info(`Unenrolling from ${slug}\n`); await manager.unenroll(slug); } } await NimbusTestUtils.waitForActiveEnrollments([]); info("Cleaning up prefs..."); Services.prefs.deleteBranch(PREF_FOO); Services.prefs.deleteBranch(PREF_BAR); await cleanup(); } }); add_task(async function test_prefFlip_setPref_restore() { const PREF = "nimbus.test-only.foo"; const SET_PREF_USER = "set-pref-user"; const SET_PREF_DEFAULT = "set-pref-default"; const PREF_FLIPS_USER = "pref-flips-user"; const PREF_FLIPS_DEFAULT = "pref-flips-default"; const FEATURE_CONFIGS = { [SET_PREF_USER]: { featureId: PREF_FEATURES[USER].featureId, value: { foo: SET_PREF_USER, }, }, [SET_PREF_DEFAULT]: { featureId: PREF_FEATURES[DEFAULT].featureId, value: { foo: SET_PREF_DEFAULT, }, }, [PREF_FLIPS_USER]: { featureId: FEATURE_ID, value: { prefs: { [PREF]: { branch: USER, value: PREF_FLIPS_USER, }, }, }, }, [PREF_FLIPS_DEFAULT]: { featureId: FEATURE_ID, value: { prefs: { [PREF]: { branch: DEFAULT, value: PREF_FLIPS_DEFAULT, }, }, }, }, }; const TEST_CASES = [ // 1. No prefs set beforehand. // - setPref first { name: "enroll in setPref on user branch and prefFlips on user branch", enrollmentOrder: [SET_PREF_USER, PREF_FLIPS_USER], expectedPrefs: { [PREF]: {} }, }, { name: "enroll in setPref on user branch and prefFlips on default branch", enrollmentOrder: [SET_PREF_USER, PREF_FLIPS_DEFAULT], expectedPrefs: { [PREF]: { defaultBranchValue: PREF_FLIPS_DEFAULT } }, }, { name: "enroll in setPref on default branch and prefFlips on user branch", enrollmentOrder: [SET_PREF_DEFAULT, PREF_FLIPS_USER], expectedPrefs: { [PREF]: { defaultBranchValue: SET_PREF_DEFAULT } }, }, { name: "enroll in setPref on default branch and prefFlips on default branch", enrollmentOrder: [SET_PREF_DEFAULT, PREF_FLIPS_DEFAULT], // We can't clear the default branch expectedPrefs: { [PREF]: { defaultBranchValue: PREF_FLIPS_DEFAULT } }, }, // - prefFlips first { name: "enroll in prefFlips on user branch and setPref on user branch", enrollmentOrder: [PREF_FLIPS_USER, SET_PREF_USER], expectedPrefs: { [PREF]: {} }, }, { name: "enroll in prefFlips on user branch and setPref on default branch", enrollmentOrder: [PREF_FLIPS_USER, SET_PREF_DEFAULT], expectedPrefs: { [PREF]: { defaultBranchValue: SET_PREF_DEFAULT } }, }, { name: "enroll in prefFlips on default branch and setPref on user branch", enrollmentOrder: [PREF_FLIPS_DEFAULT, SET_PREF_USER], expectedPrefs: { [PREF]: { defaultBranchValue: PREF_FLIPS_DEFAULT } }, }, { name: "enroll in prefFlips on default branch and setPref on default branch", enrollmentOrder: [PREF_FLIPS_DEFAULT, SET_PREF_DEFAULT], // We can't clear the default branch expectedPrefs: { [PREF]: { defaultBranchValue: SET_PREF_DEFAULT } }, }, // 2. User branch prefs set beforehand. // - setPref first { name: "set prefs on user branch and enroll in setPref on user branch and prefFlips on user branch", setPrefsBefore: { [PREF]: { userBranchValue: USER_VALUE } }, enrollmentOrder: [SET_PREF_USER, PREF_FLIPS_USER], expectedPrefs: { [PREF]: { userBranchValue: USER_VALUE } }, }, { name: "set prefs on user branch and enroll in setPref on user branch and prefFlips on default branch", setPrefsBefore: { [PREF]: { userBranchValue: USER_VALUE } }, enrollmentOrder: [SET_PREF_USER, PREF_FLIPS_DEFAULT], expectedPrefs: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: PREF_FLIPS_DEFAULT, }, }, }, { name: "set prefs on user branch and enroll in setPref on default branch and prefFlips on user branch", setPrefsBefore: { [PREF]: { userBranchValue: USER_VALUE } }, enrollmentOrder: [SET_PREF_DEFAULT, PREF_FLIPS_USER], expectedPrefs: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: SET_PREF_DEFAULT, }, }, }, { name: "set prefs on user branch and enroll in setPref on default branch and prefFlips on default branch", setPrefsBefore: { [PREF]: { userBranchValue: USER_VALUE } }, enrollmentOrder: [SET_PREF_DEFAULT, PREF_FLIPS_DEFAULT], expectedPrefs: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: PREF_FLIPS_DEFAULT, // We can't clear the default branch }, }, }, // - prefFlips first { name: "set prefs on user branch and enroll in prefFlips on user branch and setPref on user branch", setPrefsBefore: { [PREF]: { userBranchValue: USER_VALUE } }, enrollmentOrder: [PREF_FLIPS_USER, SET_PREF_USER], expectedPrefs: { [PREF]: { userBranchValue: USER_VALUE } }, }, { name: "set prefs on user branch and enroll in prefFlips on user branch and setPref on default branch", setPrefsBefore: { [PREF]: { userBranchValue: USER_VALUE } }, enrollmentOrder: [PREF_FLIPS_USER, SET_PREF_DEFAULT], expectedPrefs: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: SET_PREF_DEFAULT, }, }, }, { name: "set prefs on user branch and enroll in prefFlips on default branch and setPref on user branch", setPrefsBefore: { [PREF]: { userBranchValue: USER_VALUE } }, enrollmentOrder: [PREF_FLIPS_DEFAULT, SET_PREF_USER], expectedPrefs: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: PREF_FLIPS_DEFAULT, }, }, }, { name: "set prefs on user branch and enroll in prefFlips on default branch and setPref on default branch", setPrefsBefore: { [PREF]: { userBranchValue: USER_VALUE } }, enrollmentOrder: [PREF_FLIPS_DEFAULT, SET_PREF_DEFAULT], expectedPrefs: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: SET_PREF_DEFAULT, // We can't clear the default branch }, }, }, // 3. Default branch prefs set beforehand. // - setPref first { setPrefsBefore: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, name: "set prefs on default branch and enroll branch in setPref on user branch and prefFlips on user branch", enrollmentOrder: [SET_PREF_USER, PREF_FLIPS_USER], expectedPrefs: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, }, { setPrefsBefore: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, name: "set prefs on default branch and enroll branch in setPref on user branch and prefFlips on default branch", enrollmentOrder: [SET_PREF_USER, PREF_FLIPS_DEFAULT], expectedPrefs: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, }, { setPrefsBefore: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, name: "set prefs on default branch and enroll branch in setPref on default branch and prefFlips on user branch", enrollmentOrder: [SET_PREF_DEFAULT, PREF_FLIPS_USER], expectedPrefs: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, }, { setPrefsBefore: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, name: "set prefs on default branch and enroll branch in setPref on default branch and prefFlips on default branch", enrollmentOrder: [SET_PREF_DEFAULT, PREF_FLIPS_DEFAULT], expectedPrefs: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, }, // - prefFlips first { setPrefsBefore: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, name: "set prefs on default branch and enroll branch in prefFlips on user branch and setPref on user branch", enrollmentOrder: [PREF_FLIPS_USER, SET_PREF_USER], expectedPrefs: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, }, { name: "set prefs on default branch and enroll branch in prefFlips on user branch and setPref on default branch", setPrefsBefore: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, enrollmentOrder: [PREF_FLIPS_USER, SET_PREF_DEFAULT], expectedPrefs: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, }, { name: "set prefs on default branch and enroll branch in prefFlips on default branch and setPref on user branch", setPrefsBefore: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, enrollmentOrder: [PREF_FLIPS_DEFAULT, SET_PREF_USER], expectedPrefs: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, }, { name: "set prefs on default branch and enroll branch in prefFlips on default branch and setPref on default branch", setPrefsBefore: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, enrollmentOrder: [PREF_FLIPS_DEFAULT, SET_PREF_DEFAULT], expectedPrefs: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, }, { name: "set prefs on default branch and enroll branch in prefFlips on default branch and setPref on default branch, unenrolling in reverse order", setPrefsBefore: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, enrollmentOrder: [PREF_FLIPS_DEFAULT, SET_PREF_DEFAULT], unenrollInReverseOrder: true, expectedPrefs: { [PREF]: { defaultBranchValue: DEFAULT_VALUE } }, }, // 4. Both user and default branch prefs set beforehand. // - setPref first { setPrefsBefore: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: DEFAULT_VALUE, }, }, name: "set prefs on both branches and enroll branch in setPref on user branch and prefFlips on user branch", enrollmentOrder: [SET_PREF_USER, PREF_FLIPS_USER], expectedPrefs: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: DEFAULT_VALUE, }, }, }, { setPrefsBefore: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: DEFAULT_VALUE, }, }, name: "set prefs on both branches and enroll branch in setPref on user branch and prefFlips on default branch", enrollmentOrder: [SET_PREF_USER, PREF_FLIPS_DEFAULT], expectedPrefs: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: DEFAULT_VALUE, }, }, }, { setPrefsBefore: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: DEFAULT_VALUE, }, }, name: "set prefs on both branches and enroll branch in setPref on default branch and prefFlips on user branch", enrollmentOrder: [SET_PREF_DEFAULT, PREF_FLIPS_USER], expectedPrefs: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: DEFAULT_VALUE, }, }, }, { setPrefsBefore: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: DEFAULT_VALUE, }, }, name: "set prefs on both branches and enroll branch in setPref on default branch and prefFlips on default branch", enrollmentOrder: [SET_PREF_DEFAULT, PREF_FLIPS_DEFAULT], expectedPrefs: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: DEFAULT_VALUE, }, }, }, // - prefFlips first { setPrefsBefore: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: DEFAULT_VALUE, }, }, name: "set prefs on both branches and enroll branch in prefFlips on user branch and setPref on user branch", enrollmentOrder: [PREF_FLIPS_USER, SET_PREF_USER], expectedPrefs: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: DEFAULT_VALUE, }, }, }, { name: "set prefs on both branches and enroll branch in prefFlips on user branch and setPref on default branch", setPrefsBefore: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: DEFAULT_VALUE, }, }, enrollmentOrder: [PREF_FLIPS_USER, SET_PREF_DEFAULT], expectedPrefs: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: DEFAULT_VALUE, }, }, }, { name: "set prefs on both branches and enroll branch in prefFlips on default branch and setPref on user branch", setPrefsBefore: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: DEFAULT_VALUE, }, }, enrollmentOrder: [PREF_FLIPS_DEFAULT, SET_PREF_USER], expectedPrefs: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: DEFAULT_VALUE, }, }, }, { name: "set prefs on both branches and enroll branch in prefFlips on default branch and setPref on default branch", setPrefsBefore: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: DEFAULT_VALUE, }, }, enrollmentOrder: [PREF_FLIPS_DEFAULT, SET_PREF_DEFAULT], expectedPrefs: { [PREF]: { userBranchValue: USER_VALUE, defaultBranchValue: DEFAULT_VALUE, }, }, }, ]; for (const [i, { name, ...testCase }] of TEST_CASES.entries()) { info(`Running test case ${i}: ${name}`); const { setPrefsBefore = {}, enrollmentOrder, expectedPrefs } = testCase; info("Setting prefs before enrollment..."); setPrefs(setPrefsBefore); const { manager, cleanup } = await setupTest(); info("Enrolling..."); for (const slug of enrollmentOrder) { await NimbusTestUtils.enrollWithFeatureConfig(FEATURE_CONFIGS[slug], { manager, slug, }); } info("Checking expected enrollments..."); { const enrollment = manager.store.get(enrollmentOrder[0]); Assert.ok( enrollment !== null, `An enrollment for ${enrollmentOrder[0]} should exist` ); Assert.ok(!enrollment.active, "It should no longer be active."); } { const enrollment = manager.store.get(enrollmentOrder[1]); Assert.ok( enrollment !== null, `An enrollment for ${enrollmentOrder[1]} should exist` ); Assert.ok(enrollment.active, "It should be active."); } info("Checking submitted telemetry..."); TelemetryTestUtils.assertEvents( [ { value: enrollmentOrder[0], extra: { reason: "prefFlips-conflict", conflictingSlug: enrollmentOrder[1], }, }, ], { category: "normandy", object: "nimbus_experiment", method: "unenroll", } ); Assert.deepEqual( Glean.nimbusEvents.unenrollment.testGetValue("events").map(event => ({ reason: event.extra.reason, experiment: event.extra.experiment, conflicting_slug: event.extra.conflicting_slug, })), [ { reason: "prefFlips-conflict", experiment: enrollmentOrder[0], conflicting_slug: enrollmentOrder[1], }, ] ); Assert.deepEqual( Glean.nimbusEvents.enrollmentStatus .testGetValue("events") ?.map(ev => ev.extra), [ { slug: enrollmentOrder[0], branch: "control", status: "Enrolled", reason: "Qualified", }, { slug: enrollmentOrder[0], branch: "control", status: "Disqualified", reason: "PrefFlipsConflict", conflict_slug: enrollmentOrder[1], }, { slug: enrollmentOrder[1], branch: "control", status: "Enrolled", reason: "Qualified", }, ] ); info("Unenrolling..."); await manager.unenroll(enrollmentOrder[1]); info("Checking expected prefs..."); checkExpectedPrefBranches(expectedPrefs); await cleanup(); info("Cleaning up prefs..."); Services.prefs.deleteBranch(PREF); } }); add_task(async function test_prefFlips_cacheOriginalValues() { const recipe = NimbusTestUtils.factories.recipe.withFeatureConfig( "prefFlips-test", { featureId: FEATURE_ID, value: { prefs: { "test.pref.please.ignore": { branch: "user", value: "test-value", }, }, }, } ); const { manager, cleanup } = await setupTest(); await manager.enroll(recipe, "test"); const activeEnrollment = manager.store.getExperimentForFeature(FEATURE_ID); Assert.deepEqual(activeEnrollment.prefFlips, { originalValues: { "test.pref.please.ignore": null, }, }); const storePath = manager.store._store.path; // We are intentionally *not* forcing a save -- we are only flushing a pending // save to disk. { const jsonFile = manager.store._store; if (jsonFile._saver.isRunning) { await jsonFile._saver._runningPromise; } else if (jsonFile._saver.isArmed) { jsonFile._saver.disarm(); await jsonFile._save(); } } const storeContents = await IOUtils.readJSON(storePath); Assert.ok( Object.hasOwn(storeContents, "prefFlips-test"), "enrollment present in serialized store" ); Assert.ok( Object.hasOwn(storeContents["prefFlips-test"], "prefFlips"), "prefFlips cache preset in serialized enrollment" ); Assert.deepEqual( storeContents["prefFlips-test"].prefFlips, { originalValues: { "test.pref.please.ignore": null, }, }, "originalValues cached on serialized enrollment" ); await manager.unenroll(recipe.slug); Assert.ok( !Services.prefs.prefHasUserValue("test.pref.please.ignore"), "pref unset after unenrollment" ); await cleanup(); }); add_task(async function test_prefFlips_restore_unenroll() { const recipe = NimbusTestUtils.factories.recipe.withFeatureConfig( "prefFlips-test", { featureId: FEATURE_ID, value: { prefs: { "test.pref.please.ignore": { branch: "user", value: "test-value", }, }, }, } ); // Set up a previous ExperimentStore on disk. let storePath; { const enrollment = { slug: recipe.slug, branch: recipe.branches[0], active: true, experimentType: "nimbus", userFacingName: recipe.userFacingName, userFacingDescription: recipe.userFacingDescription, featureIds: recipe.featureIds, isRollout: recipe.isRollout, localizations: recipe.localizations, source: "rs-loader", prefFlips: { originalValues: { "test.pref.please.ignore": null, }, }, lastSeen: new Date().toJSON(), }; const store = NimbusTestUtils.stubs.store(); await store.init(); store.set(enrollment.slug, enrollment); storePath = await NimbusTestUtils.saveStore(store); } // Set the pref controlled by the experiment. Services.prefs.setStringPref("test.pref.please.ignore", "test-value"); const { manager, cleanup } = await setupTest({ storePath, secureExperiments: [recipe], }); const activeEnrollment = manager.store.getExperimentForFeature(FEATURE_ID); Assert.equal(activeEnrollment.slug, recipe.slug, "enrollment restored"); Assert.equal( manager._prefFlips._getOriginalValue("test.pref.please.ignore", "user"), null ); await manager.unenroll(recipe.slug); Assert.ok( !Services.prefs.prefHasUserValue("test.pref.please.ignore"), "pref unset after unenrollment" ); await cleanup(); }); add_task(async function test_prefFlips_failed() { const PREF = "test.pref.please.ignore"; Services.prefs.getDefaultBranch(null).setStringPref(PREF, "test-value"); const recipe = NimbusTestUtils.factories.recipe.withFeatureConfig( "prefFlips-test", { featureId: FEATURE_ID, value: { prefs: { [PREF]: { branch: "user", value: 123 }, }, }, } ); const { manager, cleanup } = await setupTest(); await manager.enroll(recipe, "test"); // Unenrolling is triggered by the PrefFlipsFeature in response to the // enrollment being added to the store and triggering the feature update // callback. // // That callback triggers an async unenroll() without awaiting (because it // wouldn't block the ExperimentStore anyway) so we have to wait for the // unenroll to be propagated to the database first. await NimbusTestUtils.waitForInactiveEnrollment(recipe.slug); const enrollment = manager.store.get(recipe.slug); Assert.ok(!enrollment.active, "Experiment should not be active"); Assert.equal(Services.prefs.getStringPref(PREF), "test-value"); TelemetryTestUtils.assertEvents( [ { value: recipe.slug, extra: { reason: "prefFlips-failed", prefName: PREF, prefType: "string", }, }, ], { category: "normandy", object: "nimbus_experiment", method: "unenroll", } ); Assert.deepEqual( Glean.nimbusEvents.unenrollment.testGetValue("events").map(event => ({ reason: event.extra.reason, experiment: event.extra.experiment, pref_name: event.extra.pref_name, pref_type: event.extra.pref_type, })), [ { reason: "prefFlips-failed", experiment: recipe.slug, pref_name: PREF, pref_type: "string", }, ] ); Services.prefs.deleteBranch(PREF); await cleanup(); }); add_task(async function test_prefFlips_failed_multiple_prefs() { const GOOD_PREF = "test.pref.please.ignore"; const BAD_PREF = "this.one.too"; Services.prefs.getDefaultBranch(null).setStringPref(BAD_PREF, "test-value"); const recipe = NimbusTestUtils.factories.recipe.withFeatureConfig( "prefFlips-test", { featureId: FEATURE_ID, value: { prefs: { [GOOD_PREF]: { branch: USER, value: 123 }, [BAD_PREF]: { branch: USER, value: 123 }, }, }, } ); const { sandbox, manager, cleanup } = await setupTest(); const setPrefSpy = sandbox.spy(PrefUtils, "setPref"); await manager.enroll(recipe, "test"); // Unenrolling is triggered by the PrefFlipsFeature in response to the // enrollment being added to the store and triggering the feature update // callback. // // That callback triggers an async unenroll() without awaiting (because it // wouldn't block the ExperimentStore anyway) so we have to wait for the // unenroll to be propagated to the database first. await NimbusTestUtils.waitForInactiveEnrollment(recipe.slug); const enrollment = manager.store.get(recipe.slug); Assert.ok(!enrollment.active, "Experiment should not be active"); Assert.deepEqual( setPrefSpy.getCall(0).args, [GOOD_PREF, 123, { branch: USER }], `should set ${GOOD_PREF}` ); Assert.deepEqual( setPrefSpy.getCall(1).args, [BAD_PREF, 123, { branch: USER }], `should have attempted to set ${BAD_PREF}` ); Assert.ok( typeof setPrefSpy.getCall(1).exception !== "undefined", `Attempting to set ${BAD_PREF} threw` ); Assert.deepEqual( setPrefSpy.getCall(2).args, [GOOD_PREF, null, { branch: USER }], `should reset ${GOOD_PREF}` ); Assert.equal( setPrefSpy.callCount, 3, "should have 3 calls to PrefUtils.setPref" ); Assert.ok( !Services.prefs.prefHasUserValue(GOOD_PREF), `${GOOD_PREF} should not be set` ); Assert.equal(Services.prefs.getStringPref(BAD_PREF), "test-value"); Services.prefs.deleteBranch(GOOD_PREF); Services.prefs.deleteBranch(BAD_PREF); await cleanup(); }); add_task(async function test_prefFlips_failed_experiment_and_rollout_1() { const ROLLOUT = "rollout"; const EXPERIMENT = "experiment"; const PREFS = { [ROLLOUT]: "test.nimbus.prefs.rollout-1", [EXPERIMENT]: "test.nimbus.prefs.experiment-1", }; const VALUES = { [ROLLOUT]: "rollout-value", [EXPERIMENT]: "experiment-value", }; const BOGUS_VALUE = 123; const TEST_CASES = [ { name: "Enrolling in an experiment and then a rollout with errors", setPrefsBefore: { [PREFS[ROLLOUT]]: { defaultBranchValue: BOGUS_VALUE }, }, enrollmentOrder: [EXPERIMENT, ROLLOUT], expectedEnrollments: [EXPERIMENT], expectedUnenrollments: [ROLLOUT], expectedPrefs: { [PREFS[EXPERIMENT]]: VALUES[EXPERIMENT], [PREFS[ROLLOUT]]: BOGUS_VALUE, }, }, ]; const FEATURE_VALUES = { [EXPERIMENT]: { prefs: { [PREFS[EXPERIMENT]]: { value: VALUES[EXPERIMENT], branch: USER, }, }, }, [ROLLOUT]: { prefs: { [PREFS[ROLLOUT]]: { value: VALUES[ROLLOUT], branch: USER, }, }, }, }; for (const [i, { name, ...testCase }] of TEST_CASES.entries()) { info(`Running test case ${i}: ${name}`); const { setPrefsBefore, enrollmentOrder, expectedEnrollments, expectedUnenrollments, expectedPrefs, } = testCase; const { manager, cleanup } = await setupTest(); info("Setting initial values of prefs..."); setPrefs(setPrefsBefore); info("Enrolling..."); for (const slug of enrollmentOrder) { await NimbusTestUtils.enrollWithFeatureConfig( { featureId: FEATURE_ID, value: FEATURE_VALUES[slug], }, { manager, slug, isRollout: slug === ROLLOUT, } ); } info("Checking expected enrollments..."); for (const slug of expectedEnrollments) { const enrollment = manager.store.get(slug); Assert.ok(enrollment.active, `The enrollment for ${slug} is active`); } await NimbusTestUtils.waitForActiveEnrollments(expectedEnrollments); info("Checking expected unenrollments..."); for (const slug of expectedUnenrollments) { await NimbusTestUtils.waitForInactiveEnrollment(slug); const enrollment = manager.store.get(slug); Assert.ok(!enrollment.active, "The enrollment is no longer active."); } info("Checking expected prefs..."); checkExpectedPrefs(expectedPrefs); info("Unenrolling..."); if (expectedEnrollments.includes(ROLLOUT)) { await manager.unenroll(ROLLOUT); } if (expectedEnrollments.includes(EXPERIMENT)) { await manager.unenroll(EXPERIMENT); } info("Cleaning up..."); Services.prefs.deleteBranch(PREFS[ROLLOUT]); Services.prefs.deleteBranch(PREFS[EXPERIMENT]); await cleanup(); } }); add_task(async function test_prefFlips_failed_experiment_and_rollout_2() { const ROLLOUT = "rollout"; const EXPERIMENT = "experiment"; const PREFS = { [ROLLOUT]: "test.nimbus.prefs.rollout-2", [EXPERIMENT]: "test.nimbus.prefs.experiment-2", }; const VALUES = { [ROLLOUT]: "rollout-value", [EXPERIMENT]: "experiment-value", }; const BOGUS_VALUE = 123; const TEST_CASES = [ { name: "Enrolling in a rollout and then an experiment with errors", setPrefsBefore: { [PREFS[EXPERIMENT]]: { defaultBranchValue: BOGUS_VALUE }, }, enrollmentOrder: [ROLLOUT, EXPERIMENT], expectedEnrollments: [ROLLOUT], expectedUnenrollments: [EXPERIMENT], expectedPrefs: { [PREFS[ROLLOUT]]: VALUES[ROLLOUT], [PREFS[EXPERIMENT]]: BOGUS_VALUE, }, }, ]; const FEATURE_VALUES = { [EXPERIMENT]: { prefs: { [PREFS[EXPERIMENT]]: { value: VALUES[EXPERIMENT], branch: USER, }, }, }, [ROLLOUT]: { prefs: { [PREFS[ROLLOUT]]: { value: VALUES[ROLLOUT], branch: USER, }, }, }, }; for (const [i, { name, ...testCase }] of TEST_CASES.entries()) { info(`Running test case ${i}: ${name}`); const { setPrefsBefore, enrollmentOrder, expectedEnrollments, expectedUnenrollments, expectedPrefs, } = testCase; const { manager, cleanup } = await setupTest(); info("Setting initial values of prefs..."); setPrefs(setPrefsBefore); info("Enrolling..."); for (const slug of enrollmentOrder) { await NimbusTestUtils.enrollWithFeatureConfig( { featureId: FEATURE_ID, value: FEATURE_VALUES[slug], }, { manager, slug, isRollout: slug === ROLLOUT, } ); } await NimbusTestUtils.waitForActiveEnrollments(expectedEnrollments); info("Checking expected enrollments..."); for (const slug of expectedEnrollments) { const enrollment = manager.store.get(slug); Assert.ok(enrollment.active, `The enrollment for ${slug} is active`); } info("Checking expected unenrollments..."); for (const slug of expectedUnenrollments) { await NimbusTestUtils.waitForInactiveEnrollment(slug); const enrollment = manager.store.get(slug); Assert.ok(!enrollment.active, "The enrollment is no longer active."); } info("Checking expected prefs..."); checkExpectedPrefs(expectedPrefs); info("Unenrolling..."); if (expectedEnrollments.includes(ROLLOUT)) { await manager.unenroll(ROLLOUT); } if (expectedEnrollments.includes(EXPERIMENT)) { await manager.unenroll(EXPERIMENT); } info("Cleaning up..."); Services.prefs.deleteBranch(PREFS[ROLLOUT]); Services.prefs.deleteBranch(PREFS[EXPERIMENT]); await cleanup(); } }); add_task(async function test_prefFlips_update_failure() { const { manager, cleanup } = await setupTest(); PrefUtils.setPref("pref.one", "default-value", { branch: DEFAULT }); PrefUtils.setPref("pref.two", "default-value", { branch: DEFAULT }); const cleanupExperiment = await NimbusTestUtils.enrollWithFeatureConfig( { featureId: FEATURE_ID, value: { prefs: { "pref.one": { value: "one", branch: USER }, "pref.two": { value: "two", branch: USER }, }, }, }, { manager, isRollout: true, slug: "rollout" } ); Assert.equal(Services.prefs.getStringPref("pref.one"), "one"); Assert.equal(Services.prefs.getStringPref("pref.two"), "two"); await NimbusTestUtils.enrollWithFeatureConfig( { featureId: FEATURE_ID, value: { prefs: { "pref.one": { value: "experiment-value", branch: USER }, "pref.two": { value: 2, branch: USER }, }, }, }, { manager, slug: "experiment" } ); await NimbusTestUtils.waitForActiveEnrollments(["rollout"]); const rolloutEnrollment = manager.store.get("rollout"); const experimentEnrollment = manager.store.get("experiment"); Assert.ok(rolloutEnrollment.active, "Rollout is active"); Assert.ok(!experimentEnrollment.active, "Experiment is inactive"); Assert.equal(experimentEnrollment.unenrollReason, "prefFlips-failed"); Assert.equal(Services.prefs.getStringPref("pref.one"), "one"); Assert.equal(Services.prefs.getStringPref("pref.two"), "two"); await cleanupExperiment(); Services.prefs.deleteBranch("pref.one"); Services.prefs.deleteBranch("pref.two"); await cleanup(); }); add_task(async function test_prefFlips_restore() { let storePath; const PREF_1 = "pref.one"; const PREF_2 = "pref.two"; const PREF_3 = "pref.three"; const PREF_4 = "pref.FOUR"; { const store = NimbusTestUtils.stubs.store(); await store.init(); store.addEnrollment( NimbusTestUtils.factories.rollout.withFeatureConfig( "rollout-1", { featureId: FEATURE_ID, value: { prefs: { [PREF_1]: { branch: USER, value: PREF_1 }, }, }, }, { prefFlips: { originalValues: { [PREF_1]: null, }, }, } ) ); store.addEnrollment( NimbusTestUtils.factories.rollout.withFeatureConfig( "rollout-2", { featureId: FEATURE_ID, value: { prefs: { [PREF_2]: { branch: USER, value: PREF_2 }, }, }, }, { prefFlips: { originalValues: { [PREF_2]: "original-pref-2-value", }, }, } ) ); store.addEnrollment( NimbusTestUtils.factories.rollout.withFeatureConfig( "rollout-3", { featureId: FEATURE_ID, value: { prefs: { [PREF_3]: { branch: DEFAULT, value: PREF_3 }, }, }, }, { prefFlips: { originalValues: { [PREF_3]: null, }, }, } ) ); store.addEnrollment( NimbusTestUtils.factories.rollout.withFeatureConfig( "rollout-4", { featureId: FEATURE_ID, value: { prefs: { [PREF_4]: { branch: DEFAULT, value: PREF_4 }, }, }, }, { prefFlips: { originalValues: { [PREF_4]: "original-pref-4-value", }, }, } ) ); storePath = await NimbusTestUtils.saveStore(store); } const { manager, cleanup } = await setupTest({ storePath }); Assert.ok(manager.store.get("rollout-1").active, "rollout-1 is active"); Assert.ok(manager.store.get("rollout-2").active, "rollout-2 is active"); Assert.ok(manager.store.get("rollout-3").active, "rollout-3 is active"); Assert.ok(manager.store.get("rollout-4").active, "rollout-4 is active"); Assert.equal( Services.prefs.getStringPref(PREF_1), PREF_1, `${PREF_1} has the correct value` ); Assert.equal( Services.prefs.getStringPref(PREF_2), PREF_2, `${PREF_2} has the correct value` ); Assert.equal( Services.prefs.getStringPref(PREF_3), PREF_3, `${PREF_3} has the correct value` ); Assert.equal( Services.prefs.getStringPref(PREF_4), PREF_4, `${PREF_4} has the correct value` ); await NimbusTestUtils.cleanupManager( ["rollout-1", "rollout-2", "rollout-3", "rollout-4"], { manager } ); Assert.equal( PrefUtils.getPref(PREF_1), null, `${PREF_1} has the correct value after unenrollment` ); Assert.equal( PrefUtils.getPref(PREF_2), "original-pref-2-value", `${PREF_2} has the correct value after unenrollment` ); Assert.equal( PrefUtils.getPref(PREF_3), PREF_3, `${PREF_3} has the correct value after unenrollment (can't reset default branch)` ); Assert.equal( PrefUtils.getPref(PREF_4), "original-pref-4-value", `${PREF_4} has the correct value` ); await cleanup(); }); add_task(async function test_prefFlips_restore_failure_conflict() { let storePath; const PREF = "pref.foo.bar"; { const store = NimbusTestUtils.stubs.store(); await store.init(); store.addEnrollment( NimbusTestUtils.factories.rollout.withFeatureConfig( "rollout-1", { featureId: FEATURE_ID, value: { prefs: { [PREF]: { branch: USER, value: "correct-value" }, }, }, }, { prefFlips: { originalValues: { [PREF]: null, }, }, } ) ); store.addEnrollment( NimbusTestUtils.factories.rollout.withFeatureConfig( "rollout-2", { featureId: FEATURE_ID, value: { prefs: { [PREF]: { branch: USER, value: "incorrect-value" }, }, }, }, { prefFlips: { originalValues: { [PREF]: null, }, }, } ) ); store.addEnrollment( NimbusTestUtils.factories.rollout.withFeatureConfig( "rollout-3", { featureId: FEATURE_ID, value: { prefs: { [PREF]: { branch: DEFAULT, value: "correct-value" }, }, }, }, { prefFlips: { originalValues: { [PREF]: null, }, }, } ) ); storePath = await NimbusTestUtils.saveStore(store); } const { manager, cleanup } = await setupTest({ storePath }); await NimbusTestUtils.waitForActiveEnrollments(["rollout-1"]); await NimbusTestUtils.waitForInactiveEnrollment("rollout-2"); Assert.ok(manager.store.get("rollout-1").active, "rollout-1 is active"); Assert.ok(!manager.store.get("rollout-2").active, "rollout-2 is not active"); Assert.equal( manager.store.get("rollout-2").unenrollReason, "prefFlips-failed" ); Assert.ok(!manager.store.get("rollout-3").active, "rollout-3 is not active"); Assert.equal( manager.store.get("rollout-3").unenrollReason, "prefFlips-failed" ); Assert.equal( PrefUtils.getPref(PREF), "correct-value", `${PREF} has the correct value` ); await NimbusTestUtils.cleanupManager(["rollout-1"], { manager }); Assert.equal( PrefUtils.getPref(PREF), null, `${PREF} has the correct value after unenrollment` ); await cleanup(); }); // Test the case where an experiment sets a default branch pref, but the user // changed their user.js between restarts. add_task(async function test_prefFlips_restore_failure_wrong_type() { const PREF_1 = "foo.bar.baz"; const PREF_2 = "qux.quux.corge.grault"; const recipe = NimbusTestUtils.factories.recipe.withFeatureConfig( "prefFlips-test", { featureId: FEATURE_ID, value: { prefs: { [PREF_1]: { branch: DEFAULT, value: "recipe-value", }, [PREF_2]: { branch: USER, value: "recipe-value", }, }, }, } ); let storePath; { const prevEnrollment = { slug: recipe.slug, branch: recipe.branches[0], active: true, experimentType: "nimbus", userFacingName: recipe.userFacingName, userFacingDescription: recipe.userFacingDescription, featureIds: recipe.featureIds, isRollout: recipe.isRollout, localizations: recipe.localizations, source: "rs-loader", prefFlips: { originalValues: { [PREF_1]: "original-value", [PREF_2]: "original-value", }, }, lastSeen: new Date().toJSON(), }; const store = NimbusTestUtils.stubs.store(); await store.init(); store.set(prevEnrollment.slug, prevEnrollment); storePath = await NimbusTestUtils.saveStore(store); } Services.prefs.setIntPref(PREF_1, 123); const { manager, cleanup } = await setupTest({ storePath, secureExperiments: [recipe], }); await NimbusTestUtils.waitForInactiveEnrollment(recipe.slug); const enrollment = manager.store.get(recipe.slug); Assert.ok(!enrollment.active, "Enrollment should be inactive"); Assert.equal(enrollment.unenrollReason, "prefFlips-failed"); Assert.ok( !Services.prefs.prefHasDefaultValue(PREF_1), `${PREF_1} has no default value` ); Assert.ok( !Services.prefs.prefHasDefaultValue(PREF_2), `${PREF_2} has no default value` ); Assert.equal( Services.prefs.getIntPref(PREF_1), 123, `${PREF_1} value unchanged` ); Assert.ok(!Services.prefs.prefHasUserValue(PREF_2), `${PREF_2} has no value`); Services.prefs.deleteBranch(PREF_1); Services.prefs.deleteBranch(PREF_2); await cleanup(); }); add_task( async function test_prefFlips_reenroll_set_default_branch_wrong_type() { const PREF = "test.pref.please.ignore"; const recipe = NimbusTestUtils.factories.recipe.withFeatureConfig( "prefFlips-test", { featureId: FEATURE_ID, value: { prefs: { [PREF]: { value: 123, branch: DEFAULT }, }, }, }, { isRollout: true } ); const { manager, cleanup } = await setupTest(); PrefUtils.setPref(PREF, "default-value", { branch: DEFAULT }); await manager.enroll(recipe, "rs-loader"); await NimbusTestUtils.waitForInactiveEnrollment(recipe.slug); let enrollment = manager.store.get(recipe.slug); Assert.ok(!enrollment.active, "enrollment should not be active"); Assert.equal(enrollment.unenrollReason, "prefFlips-failed"); await manager.enroll(recipe, "rs-loader", { reenroll: true }); await NimbusTestUtils.waitForInactiveEnrollment(recipe.slug); enrollment = manager.store.get(recipe.slug); Assert.ok(!enrollment.active, "enrollment should not be active"); Assert.equal(enrollment.unenrollReason, "prefFlips-failed"); Services.prefs.deleteBranch(PREF); await cleanup(); } ); add_task(async function testDb() { const { manager, cleanup } = await setupTest(); PrefUtils.setPref("foo.bar.baz", "foo"); await manager.enroll( NimbusTestUtils.factories.recipe.withFeatureConfig("slug", { featureId: "prefFlips", value: { prefs: { "foo.bar.baz": { value: "bar", branch: "user", }, }, }, }), "test" ); const conn = await ProfilesDatastoreService.getConnection(); const [result] = await conn.execute( ` SELECT json(prefFlips) as prefFlips FROM NimbusEnrollments WHERE profileId = :profileId AND slug = :slug; `, { slug: "slug", profileId: ExperimentAPI.profileId, } ); const prefFlips = JSON.parse(result.getResultByName("prefFlips")); const enrollment = manager.store.get("slug"); Assert.deepEqual( prefFlips, enrollment.prefFlips, "prefFlips stored in the database" ); Assert.deepEqual(prefFlips, { originalValues: { "foo.bar.baz": "foo", }, }); await manager.unenroll("slug"); await cleanup(); Services.prefs.deleteBranch("foo.bar.baz"); });