diff options
Diffstat (limited to 'toolkit/components/nimbus/test/unit/test_ExperimentManager_enroll.js')
-rw-r--r-- | toolkit/components/nimbus/test/unit/test_ExperimentManager_enroll.js | 984 |
1 files changed, 984 insertions, 0 deletions
diff --git a/toolkit/components/nimbus/test/unit/test_ExperimentManager_enroll.js b/toolkit/components/nimbus/test/unit/test_ExperimentManager_enroll.js new file mode 100644 index 0000000000..b5310f7835 --- /dev/null +++ b/toolkit/components/nimbus/test/unit/test_ExperimentManager_enroll.js @@ -0,0 +1,984 @@ +"use strict"; + +const { NormandyTestUtils } = ChromeUtils.import( + "resource://testing-common/NormandyTestUtils.jsm" +); +const { Sampling } = ChromeUtils.import( + "resource://gre/modules/components-utils/Sampling.jsm" +); +const { ClientEnvironment } = ChromeUtils.import( + "resource://normandy/lib/ClientEnvironment.jsm" +); +const { cleanupStorePrefCache } = ExperimentFakes; + +const { ExperimentStore } = ChromeUtils.import( + "resource://nimbus/lib/ExperimentStore.jsm" +); +const { TelemetryEnvironment } = ChromeUtils.importESModule( + "resource://gre/modules/TelemetryEnvironment.sys.mjs" +); +const { TelemetryEvents } = ChromeUtils.import( + "resource://normandy/lib/TelemetryEvents.jsm" +); + +const { SYNC_DATA_PREF_BRANCH, SYNC_DEFAULTS_PREF_BRANCH } = ExperimentStore; + +const globalSandbox = sinon.createSandbox(); +globalSandbox.spy(TelemetryEnvironment, "setExperimentInactive"); +globalSandbox.spy(TelemetryEvents, "sendEvent"); +registerCleanupFunction(() => { + globalSandbox.restore(); +}); + +async function assertEmptyStore(store) { + Assert.deepEqual( + store + .getAll() + .filter(e => e.active) + .map(e => e.slug), + [], + "Store should have no active enrollments" + ); + + Assert.deepEqual( + store + .getAll() + .filter(e => e.inactive) + .map(e => e.slug), + [], + "Store should have no inactive enrollments" + ); + + store._store.saveSoon(); + await store._store.finalize(); + await IOUtils.remove(store._store.path); +} + +/** + * FOG requires a little setup in order to test it + */ +add_setup(function test_setup() { + // FOG needs a profile directory to put its data in. + do_get_profile(); + + // FOG needs to be initialized in order for data to flow. + Services.fog.initializeFOG(); +}); + +/** + * The normal case: Enrollment of a new experiment + */ +add_task(async function test_add_to_store() { + const manager = ExperimentFakes.manager(); + const recipe = ExperimentFakes.recipe("foo"); + const enrollPromise = new Promise(resolve => + manager.store.on("update:foo", resolve) + ); + + await manager.onStartup(); + + await manager.enroll(recipe, "test_add_to_store"); + await enrollPromise; + const experiment = manager.store.get("foo"); + + Assert.ok(experiment, "should add an experiment with slug foo"); + Assert.ok( + recipe.branches.includes(experiment.branch), + "should choose a branch from the recipe.branches" + ); + Assert.equal(experiment.active, true, "should set .active = true"); + Assert.ok( + NormandyTestUtils.isUuid(experiment.enrollmentId), + "should add a valid enrollmentId" + ); + + manager.unenroll("foo", "test-cleanup"); + + await assertEmptyStore(manager.store); +}); + +add_task(async function test_add_rollout_to_store() { + const manager = ExperimentFakes.manager(); + const recipe = { + ...ExperimentFakes.recipe("rollout-slug"), + branches: [ExperimentFakes.rollout("rollout").branch], + isRollout: true, + active: true, + bucketConfig: { + namespace: "nimbus-test-utils", + randomizationUnit: "normandy_id", + start: 0, + count: 1000, + total: 1000, + }, + }; + const enrollPromise = new Promise(resolve => + manager.store.on("update:rollout-slug", resolve) + ); + + await manager.onStartup(); + + await manager.enroll(recipe, "test_add_rollout_to_store"); + await enrollPromise; + const experiment = manager.store.get("rollout-slug"); + + Assert.ok(experiment, `Should add an experiment with slug ${recipe.slug}`); + Assert.ok( + recipe.branches.includes(experiment.branch), + "should choose a branch from the recipe.branches" + ); + Assert.equal(experiment.isRollout, true, "should have .isRollout"); + + manager.unenroll("rollout-slug", "test-cleanup"); + + await assertEmptyStore(manager.store); +}); + +add_task( + async function test_setExperimentActive_sendEnrollmentTelemetry_called() { + const manager = ExperimentFakes.manager(); + const sandbox = sinon.createSandbox(); + const enrollPromise = new Promise(resolve => + manager.store.on("update:foo", resolve) + ); + sandbox.spy(manager, "setExperimentActive"); + sandbox.spy(manager, "sendEnrollmentTelemetry"); + + // Clear any pre-existing data in Glean + Services.fog.testResetFOG(); + + await manager.onStartup(); + + // Ensure there is no experiment active with the id in FOG + Assert.equal( + undefined, + Services.fog.testGetExperimentData("foo"), + "no active experiment exists before enrollment" + ); + + // Check that there aren't any Glean enrollment events yet + var enrollmentEvents = Glean.nimbusEvents.enrollment.testGetValue(); + Assert.equal( + undefined, + enrollmentEvents, + "no Glean enrollment events before enrollment" + ); + + await manager.enroll( + ExperimentFakes.recipe("foo"), + "test_setExperimentActive_sendEnrollmentTelemetry_called" + ); + await enrollPromise; + const experiment = manager.store.get("foo"); + + Assert.equal( + manager.setExperimentActive.calledWith(experiment), + true, + "should call setExperimentActive after an enrollment" + ); + + Assert.equal( + manager.sendEnrollmentTelemetry.calledWith(experiment), + true, + "should call sendEnrollmentTelemetry after an enrollment" + ); + + // Test Glean experiment API interaction + Assert.notEqual( + undefined, + Services.fog.testGetExperimentData(experiment.slug), + "Glean.setExperimentActive called with `foo` feature" + ); + + // Check that the Glean enrollment event was recorded. + enrollmentEvents = Glean.nimbusEvents.enrollment.testGetValue(); + // We expect only one event + Assert.equal(1, enrollmentEvents.length); + // And that one event matches the expected enrolled experiment + Assert.equal( + experiment.slug, + enrollmentEvents[0].extra.experiment, + "Glean.nimbusEvents.enrollment recorded with correct experiment slug" + ); + Assert.equal( + experiment.branch.slug, + enrollmentEvents[0].extra.branch, + "Glean.nimbusEvents.enrollment recorded with correct branch slug" + ); + Assert.equal( + experiment.experimentType, + enrollmentEvents[0].extra.experiment_type, + "Glean.nimbusEvents.enrollment recorded with correct experiment type" + ); + Assert.equal( + experiment.enrollmentId, + enrollmentEvents[0].extra.enrollment_id, + "Glean.nimbusEvents.enrollment recorded with correct enrollment id" + ); + + manager.unenroll("foo", "test-cleanup"); + + await assertEmptyStore(manager.store); + } +); + +add_task(async function test_setRolloutActive_sendEnrollmentTelemetry_called() { + globalSandbox.reset(); + globalSandbox.spy(TelemetryEnvironment, "setExperimentActive"); + globalSandbox.spy(TelemetryEvents.sendEvent); + const manager = ExperimentFakes.manager(); + const sandbox = sinon.createSandbox(); + const rolloutRecipe = { + ...ExperimentFakes.recipe("rollout"), + branches: [ExperimentFakes.rollout("rollout").branch], + isRollout: true, + }; + const enrollPromise = new Promise(resolve => + manager.store.on("update:rollout", resolve) + ); + sandbox.spy(manager, "setExperimentActive"); + sandbox.spy(manager, "sendEnrollmentTelemetry"); + + // Clear any pre-existing data in Glean + Services.fog.testResetFOG(); + + await manager.onStartup(); + + // Test Glean experiment API interaction + Assert.equal( + undefined, + Services.fog.testGetExperimentData("rollout"), + "no rollout active before enrollment" + ); + + // Check that there aren't any Glean enrollment events yet + var enrollmentEvents = Glean.nimbusEvents.enrollment.testGetValue(); + Assert.equal( + undefined, + enrollmentEvents, + "no Glean enrollment events before enrollment" + ); + + let result = await manager.enroll( + rolloutRecipe, + "test_setRolloutActive_sendEnrollmentTelemetry_called" + ); + + await enrollPromise; + + const enrollment = manager.store.get("rollout"); + + Assert.ok(!!result && !!enrollment, "Enrollment was successful"); + + Assert.equal( + TelemetryEnvironment.setExperimentActive.called, + true, + "should call setExperimentActive" + ); + Assert.ok( + manager.setExperimentActive.calledWith(enrollment), + "Should call setExperimentActive with the rollout" + ); + Assert.equal( + manager.setExperimentActive.firstCall.args[0].experimentType, + "rollout", + "Should have the correct experimentType" + ); + Assert.equal( + manager.sendEnrollmentTelemetry.calledWith(enrollment), + true, + "should call sendEnrollmentTelemetry after an enrollment" + ); + Assert.ok( + TelemetryEvents.sendEvent.calledOnce, + "Should send out enrollment telemetry" + ); + Assert.ok( + TelemetryEvents.sendEvent.calledWith( + "enroll", + sinon.match.string, + enrollment.slug, + { + experimentType: "rollout", + branch: enrollment.branch.slug, + enrollmentId: enrollment.enrollmentId, + } + ), + "Should send telemetry with expected values" + ); + + // Test Glean experiment API interaction + Assert.equal( + enrollment.branch.slug, + Services.fog.testGetExperimentData(enrollment.slug).branch, + "Glean.setExperimentActive called with expected values" + ); + + // Check that the Glean enrollment event was recorded. + enrollmentEvents = Glean.nimbusEvents.enrollment.testGetValue(); + // We expect only one event + Assert.equal(1, enrollmentEvents.length); + // And that one event matches the expected enrolled experiment + Assert.equal( + enrollment.slug, + enrollmentEvents[0].extra.experiment, + "Glean.nimbusEvents.enrollment recorded with correct experiment slug" + ); + Assert.equal( + enrollment.branch.slug, + enrollmentEvents[0].extra.branch, + "Glean.nimbusEvents.enrollment recorded with correct branch slug" + ); + Assert.equal( + enrollment.experimentType, + enrollmentEvents[0].extra.experiment_type, + "Glean.nimbusEvents.enrollment recorded with correct experiment type" + ); + Assert.equal( + enrollment.enrollmentId, + enrollmentEvents[0].extra.enrollment_id, + "Glean.nimbusEvents.enrollment recorded with correct enrollment id" + ); + + manager.unenroll("rollout", "test-cleanup"); + + await assertEmptyStore(manager.store); + + globalSandbox.restore(); +}); + +// /** +// * Failure cases: +// * - slug conflict +// * - group conflict +// */ + +add_task(async function test_failure_name_conflict() { + const manager = ExperimentFakes.manager(); + const sandbox = sinon.createSandbox(); + sandbox.spy(manager, "sendFailureTelemetry"); + + // Clear any pre-existing data in Glean + Services.fog.testResetFOG(); + + await manager.onStartup(); + + // Check that there aren't any Glean enroll_failed events yet + var failureEvents = Glean.nimbusEvents.enrollFailed.testGetValue(); + Assert.equal( + undefined, + failureEvents, + "no Glean enroll_failed events before failure" + ); + + // simulate adding a previouly enrolled experiment + await manager.store.addEnrollment(ExperimentFakes.experiment("foo")); + + await Assert.rejects( + manager.enroll(ExperimentFakes.recipe("foo"), "test_failure_name_conflict"), + /An experiment with the slug "foo" already exists/, + "should throw if a conflicting experiment exists" + ); + + Assert.equal( + manager.sendFailureTelemetry.calledWith( + "enrollFailed", + "foo", + "name-conflict" + ), + true, + "should send failure telemetry if a conflicting experiment exists" + ); + + // Check that the Glean enrollment event was recorded. + failureEvents = Glean.nimbusEvents.enrollFailed.testGetValue(); + // We expect only one event + Assert.equal(1, failureEvents.length); + // And that one event matches the expected enrolled experiment + Assert.equal( + "foo", + failureEvents[0].extra.experiment, + "Glean.nimbusEvents.enroll_failed recorded with correct experiment slug" + ); + Assert.equal( + "name-conflict", + failureEvents[0].extra.reason, + "Glean.nimbusEvents.enroll_failed recorded with correct reason" + ); + + manager.unenroll("foo", "test-cleanup"); + + await assertEmptyStore(manager.store); +}); + +add_task(async function test_failure_group_conflict() { + const manager = ExperimentFakes.manager(); + const sandbox = sinon.createSandbox(); + sandbox.spy(manager, "sendFailureTelemetry"); + + // Clear any pre-existing data in Glean + Services.fog.testResetFOG(); + + await manager.onStartup(); + + // Check that there aren't any Glean enroll_failed events yet + var failureEvents = Glean.nimbusEvents.enrollFailed.testGetValue(); + Assert.equal( + undefined, + failureEvents, + "no Glean enroll_failed events before failure" + ); + + // Two conflicting branches that both have the group "pink" + // These should not be allowed to exist simultaneously. + const existingBranch = { + slug: "treatment", + features: [{ featureId: "pink", value: {} }], + }; + const newBranch = { + slug: "treatment", + features: [{ featureId: "pink", value: {} }], + }; + + // simulate adding an experiment with a conflicting group "pink" + await manager.store.addEnrollment( + ExperimentFakes.experiment("foo", { + branch: existingBranch, + }) + ); + + // ensure .enroll chooses the special branch with the conflict + sandbox.stub(manager, "chooseBranch").returns(newBranch); + Assert.equal( + await manager.enroll( + ExperimentFakes.recipe("bar", { branches: [newBranch] }), + "test_failure_group_conflict" + ), + null, + "should not enroll if there is a feature conflict" + ); + + Assert.equal( + manager.sendFailureTelemetry.calledWith( + "enrollFailed", + "bar", + "feature-conflict" + ), + true, + "should send failure telemetry if a feature conflict exists" + ); + + // Check that the Glean enroll_failed event was recorded. + failureEvents = Glean.nimbusEvents.enrollFailed.testGetValue(); + // We expect only one event + Assert.equal(1, failureEvents.length); + // And that event matches the expected experiment and reason + Assert.equal( + "bar", + failureEvents[0].extra.experiment, + "Glean.nimbusEvents.enroll_failed recorded with correct experiment slug" + ); + Assert.equal( + "feature-conflict", + failureEvents[0].extra.reason, + "Glean.nimbusEvents.enroll_failed recorded with correct reason" + ); + + manager.unenroll("foo", "test-cleanup"); + + await assertEmptyStore(manager.store); +}); + +add_task(async function test_rollout_failure_group_conflict() { + const manager = ExperimentFakes.manager(); + const sandbox = sinon.createSandbox(); + const rollout = ExperimentFakes.rollout("rollout-enrollment"); + const recipe = { + ...ExperimentFakes.recipe("rollout-recipe"), + branches: [rollout.branch], + isRollout: true, + }; + sandbox.spy(manager, "sendFailureTelemetry"); + + // Clear any pre-existing data in Glean + Services.fog.testResetFOG(); + + await manager.onStartup(); + + // Check that there aren't any Glean enroll_failed events yet + var failureEvents = Glean.nimbusEvents.enrollFailed.testGetValue(); + Assert.equal( + undefined, + failureEvents, + "no Glean enroll_failed events before failure" + ); + + // simulate adding an experiment with a conflicting group "pink" + await manager.store.addEnrollment(rollout); + + Assert.equal( + await manager.enroll(recipe, "test_rollout_failure_group_conflict"), + null, + "should not enroll if there is a feature conflict" + ); + + Assert.equal( + manager.sendFailureTelemetry.calledWith( + "enrollFailed", + recipe.slug, + "feature-conflict" + ), + true, + "should send failure telemetry if a feature conflict exists" + ); + + // Check that the Glean enroll_failed event was recorded. + failureEvents = Glean.nimbusEvents.enrollFailed.testGetValue(); + // We expect only one event + Assert.equal(1, failureEvents.length); + // And that event matches the expected experiment and reason + Assert.equal( + recipe.slug, + failureEvents[0].extra.experiment, + "Glean.nimbusEvents.enroll_failed recorded with correct experiment slug" + ); + Assert.equal( + "feature-conflict", + failureEvents[0].extra.reason, + "Glean.nimbusEvents.enroll_failed recorded with correct reason" + ); + + manager.unenroll("rollout-enrollment", "test-cleanup"); + + await assertEmptyStore(manager.store); +}); + +add_task(async function test_rollout_experiment_no_conflict() { + const manager = ExperimentFakes.manager(); + const sandbox = sinon.createSandbox(); + const experiment = ExperimentFakes.recipe("experiment"); + const rollout = ExperimentFakes.recipe("rollout", { isRollout: true }); + + sandbox.spy(manager, "sendFailureTelemetry"); + + // Clear any pre-existing data in Glean + Services.fog.testResetFOG(); + + await manager.onStartup(); + + // Check that there aren't any Glean enroll_failed events yet + var failureEvents = Glean.nimbusEvents.enrollFailed.testGetValue(); + Assert.equal( + undefined, + failureEvents, + "no Glean enroll_failed events before failure" + ); + + await ExperimentFakes.enrollmentHelper(experiment, { + manager, + }).enrollmentPromise; + await ExperimentFakes.enrollmentHelper(rollout, { + manager, + }).enrollmentPromise; + + Assert.ok( + manager.store.get(experiment.slug).active, + "Enrolled in the experiment for the feature" + ); + + Assert.ok( + manager.store.get(rollout.slug).active, + "Enrolled in the rollout for the feature" + ); + + Assert.ok( + manager.sendFailureTelemetry.notCalled, + "Should send failure telemetry if a feature conflict exists" + ); + + // Check that there aren't any Glean enroll_failed events + failureEvents = Glean.nimbusEvents.enrollFailed.testGetValue(); + Assert.equal( + undefined, + failureEvents, + "no Glean enroll_failed events before failure" + ); + + await ExperimentFakes.cleanupAll([experiment.slug, rollout.slug], { + manager, + }); + + await assertEmptyStore(manager.store); +}); + +add_task(async function test_sampling_check() { + const manager = ExperimentFakes.manager(); + let recipe = ExperimentFakes.recipe("foo", { bucketConfig: null }); + const sandbox = sinon.createSandbox(); + sandbox.stub(Sampling, "bucketSample").resolves(true); + sandbox.replaceGetter(ClientEnvironment, "userId", () => 42); + + Assert.ok( + !manager.isInBucketAllocation(recipe.bucketConfig), + "fails for no bucket config" + ); + + recipe = ExperimentFakes.recipe("foo2", { + bucketConfig: { randomizationUnit: "foo" }, + }); + + Assert.ok( + !manager.isInBucketAllocation(recipe.bucketConfig), + "fails for unknown randomizationUnit" + ); + + recipe = ExperimentFakes.recipe("foo3"); + + const result = await manager.isInBucketAllocation(recipe.bucketConfig); + + Assert.equal( + Sampling.bucketSample.callCount, + 1, + "it should call bucketSample" + ); + Assert.ok(result, "result should be true"); + const { args } = Sampling.bucketSample.firstCall; + Assert.equal(args[0][0], 42, "called with expected randomization id"); + Assert.equal( + args[0][1], + recipe.bucketConfig.namespace, + "called with expected namespace" + ); + Assert.equal( + args[1], + recipe.bucketConfig.start, + "called with expected start" + ); + Assert.equal( + args[2], + recipe.bucketConfig.count, + "called with expected count" + ); + Assert.equal( + args[3], + recipe.bucketConfig.total, + "called with expected total" + ); + + await assertEmptyStore(manager.store); + + sandbox.reset(); +}); + +add_task(async function enroll_in_reference_aw_experiment() { + cleanupStorePrefCache(); + + let dir = Services.dirsvc.get("CurWorkD", Ci.nsIFile).path; + let src = PathUtils.join( + dir, + "reference_aboutwelcome_experiment_content.json" + ); + const content = await IOUtils.readJSON(src); + // Create two dummy branches with the content from disk + const branches = ["treatment-a", "treatment-b"].map(slug => ({ + slug, + ratio: 1, + features: [ + { value: { ...content, enabled: true }, featureId: "aboutwelcome" }, + ], + })); + let recipe = ExperimentFakes.recipe("reference-aw", { branches }); + // Ensure we get enrolled + recipe.bucketConfig.count = recipe.bucketConfig.total; + + const manager = ExperimentFakes.manager(); + const enrollPromise = new Promise(resolve => + manager.store.on("update:reference-aw", resolve) + ); + await manager.onStartup(); + await manager.enroll(recipe, "enroll_in_reference_aw_experiment"); + await enrollPromise; + + Assert.ok(manager.store.get("reference-aw"), "Successful onboarding"); + let prefValue = Services.prefs.getStringPref( + `${SYNC_DATA_PREF_BRANCH}aboutwelcome` + ); + Assert.ok( + prefValue, + "aboutwelcome experiment enrollment should be stored to prefs" + ); + // In case some regression causes us to store a significant amount of data + // in prefs. + Assert.ok(prefValue.length < 3498, "Make sure we don't bloat the prefs"); + + manager.unenroll(recipe.slug, "enroll_in_reference_aw_experiment:cleanup"); + manager.store._deleteForTests("aboutwelcome"); + + await assertEmptyStore(manager.store); +}); + +add_task(async function test_forceEnroll_cleanup() { + const manager = ExperimentFakes.manager(); + const sandbox = sinon.createSandbox(); + const fooEnrollPromise = new Promise(resolve => + manager.store.on("update:foo", resolve) + ); + const barEnrollPromise = new Promise(resolve => + manager.store.on("update:optin-bar", resolve) + ); + let unenrollStub = sandbox.spy(manager, "unenroll"); + let existingRecipe = ExperimentFakes.recipe("foo", { + branches: [ + { + slug: "treatment", + ratio: 1, + features: [{ featureId: "force-enrollment", value: {} }], + }, + ], + }); + let forcedRecipe = ExperimentFakes.recipe("bar", { + branches: [ + { + slug: "treatment", + ratio: 1, + features: [{ featureId: "force-enrollment", value: {} }], + }, + ], + }); + + await manager.onStartup(); + await manager.enroll(existingRecipe, "test_forceEnroll_cleanup"); + await fooEnrollPromise; + + let setExperimentActiveSpy = sandbox.spy(manager, "setExperimentActive"); + manager.forceEnroll(forcedRecipe, forcedRecipe.branches[0]); + await barEnrollPromise; + + Assert.ok(unenrollStub.called, "Unenrolled from existing experiment"); + Assert.equal( + unenrollStub.firstCall.args[0], + existingRecipe.slug, + "Called with existing recipe slug" + ); + Assert.ok(setExperimentActiveSpy.calledOnce, "Activated forced experiment"); + Assert.equal( + setExperimentActiveSpy.firstCall.args[0].slug, + `optin-${forcedRecipe.slug}`, + "Called with forced experiment slug" + ); + Assert.equal( + manager.store.getExperimentForFeature("force-enrollment").slug, + `optin-${forcedRecipe.slug}`, + "Enrolled in forced experiment" + ); + + manager.unenroll(`optin-${forcedRecipe.slug}`, "test-cleanup"); + + await assertEmptyStore(manager.store); + + sandbox.restore(); +}); + +add_task(async function test_rollout_unenroll_conflict() { + const manager = ExperimentFakes.manager(); + const sandbox = sinon.createSandbox(); + let unenrollStub = sandbox.stub(manager, "unenroll").returns(true); + let enrollStub = sandbox.stub(manager, "_enroll").returns(true); + let rollout = ExperimentFakes.rollout("rollout_conflict"); + + // We want to force a conflict + sandbox.stub(manager.store, "getRolloutForFeature").returns(rollout); + + manager.forceEnroll(rollout, rollout.branch); + + Assert.ok(unenrollStub.calledOnce, "Should unenroll the conflicting rollout"); + Assert.ok( + unenrollStub.calledWith(rollout.slug, "force-enrollment"), + "Should call with expected slug" + ); + Assert.ok(enrollStub.calledOnce, "Should call enroll as expected"); + + await assertEmptyStore(manager.store); + + sandbox.restore(); +}); + +add_task(async function test_forceEnroll() { + const experiment1 = ExperimentFakes.recipe("experiment-1"); + const experiment2 = ExperimentFakes.recipe("experiment-2"); + const rollout1 = ExperimentFakes.recipe("rollout-1", { isRollout: true }); + const rollout2 = ExperimentFakes.recipe("rollout-2", { isRollout: true }); + + const TEST_CASES = [ + { + enroll: [experiment1, rollout1], + expected: [experiment1, rollout1], + }, + { + enroll: [rollout1, experiment1], + expected: [experiment1, rollout1], + }, + { + enroll: [experiment1, experiment2], + expected: [experiment2], + }, + { + enroll: [rollout1, rollout2], + expected: [rollout2], + }, + { + enroll: [experiment1, rollout1, rollout2, experiment2], + expected: [experiment2, rollout2], + }, + ]; + + async function forceEnroll(manager, recipe) { + const enrollmentPromise = new Promise(resolve => { + manager.store.on(`update:optin-${recipe.slug}`, resolve); + }); + + manager.forceEnroll(recipe, recipe.branches[0]); + + return enrollmentPromise; + } + + const loader = ExperimentFakes.rsLoader(); + const manager = loader.manager; + + sinon + .stub(loader.remoteSettingsClient, "get") + .resolves([experiment1, experiment2, rollout1, rollout2]); + sinon.stub(loader, "setTimer"); + + await loader.init(); + await manager.onStartup(); + + for (const { enroll, expected } of TEST_CASES) { + for (const recipe of enroll) { + await forceEnroll(manager, recipe); + } + + const activeSlugs = manager.store + .getAll() + .filter(enrollment => enrollment.active) + .map(r => r.slug); + + Assert.equal( + activeSlugs.length, + expected.length, + `Should be enrolled in ${expected.length} experiments and rollouts` + ); + + for (const { slug, isRollout } of expected) { + Assert.ok( + activeSlugs.includes(`optin-${slug}`), + `Should be enrolled in ${ + isRollout ? "rollout" : "experiment" + } with slug optin-${slug}` + ); + } + + for (const { slug } of expected) { + manager.unenroll(`optin-${slug}`); + manager.store._deleteForTests(`optin-${slug}`); + } + } + + await assertEmptyStore(manager.store); +}); + +add_task(async function test_featureIds_is_stored() { + Services.prefs.setStringPref("messaging-system.log", "all"); + const recipe = ExperimentFakes.recipe("featureIds"); + // Ensure we get enrolled + recipe.bucketConfig.count = recipe.bucketConfig.total; + const store = ExperimentFakes.store(); + const manager = ExperimentFakes.manager(store); + + await manager.onStartup(); + + const { + enrollmentPromise, + doExperimentCleanup, + } = ExperimentFakes.enrollmentHelper(recipe, { manager }); + + await enrollmentPromise; + + Assert.ok(manager.store.addEnrollment.calledOnce, "experiment is stored"); + let [enrollment] = manager.store.addEnrollment.firstCall.args; + Assert.ok("featureIds" in enrollment, "featureIds is stored"); + Assert.deepEqual( + enrollment.featureIds, + ["testFeature"], + "Has expected value" + ); + + await doExperimentCleanup(); + + await assertEmptyStore(manager.store); +}); + +add_task(async function experiment_and_rollout_enroll_and_cleanup() { + let store = ExperimentFakes.store(); + const manager = ExperimentFakes.manager(store); + + await manager.onStartup(); + + let rolloutCleanup = await ExperimentFakes.enrollWithRollout( + { + featureId: "aboutwelcome", + value: { enabled: true }, + }, + { + manager, + } + ); + + let experimentCleanup = await ExperimentFakes.enrollWithFeatureConfig( + { + featureId: "aboutwelcome", + value: { enabled: true }, + }, + { manager } + ); + + Assert.ok( + Services.prefs.getBoolPref(`${SYNC_DATA_PREF_BRANCH}aboutwelcome.enabled`) + ); + Assert.ok( + Services.prefs.getBoolPref( + `${SYNC_DEFAULTS_PREF_BRANCH}aboutwelcome.enabled` + ) + ); + + await experimentCleanup(); + + Assert.ok( + !Services.prefs.getBoolPref( + `${SYNC_DATA_PREF_BRANCH}aboutwelcome.enabled`, + false + ) + ); + Assert.ok( + Services.prefs.getBoolPref( + `${SYNC_DEFAULTS_PREF_BRANCH}aboutwelcome.enabled` + ) + ); + + await rolloutCleanup(); + + Assert.ok( + !Services.prefs.getBoolPref( + `${SYNC_DATA_PREF_BRANCH}aboutwelcome.enabled`, + false + ) + ); + Assert.ok( + !Services.prefs.getBoolPref( + `${SYNC_DEFAULTS_PREF_BRANCH}aboutwelcome.enabled`, + false + ) + ); + + await assertEmptyStore(manager.store); +}); |