1
0
Fork 0
firefox/toolkit/components/nimbus/test/unit/test_ExperimentManager_prefs.js
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

3545 lines
93 KiB
JavaScript

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