summaryrefslogtreecommitdiffstats
path: root/toolkit/components/messaging-system/test/unit/test_ExperimentManager_enroll.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/messaging-system/test/unit/test_ExperimentManager_enroll.js
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/messaging-system/test/unit/test_ExperimentManager_enroll.js')
-rw-r--r--toolkit/components/messaging-system/test/unit/test_ExperimentManager_enroll.js233
1 files changed, 233 insertions, 0 deletions
diff --git a/toolkit/components/messaging-system/test/unit/test_ExperimentManager_enroll.js b/toolkit/components/messaging-system/test/unit/test_ExperimentManager_enroll.js
new file mode 100644
index 0000000000..bd7601215f
--- /dev/null
+++ b/toolkit/components/messaging-system/test/unit/test_ExperimentManager_enroll.js
@@ -0,0 +1,233 @@
+"use strict";
+
+const { ExperimentFakes } = ChromeUtils.import(
+ "resource://testing-common/MSTestUtils.jsm"
+);
+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 { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+
+/**
+ * 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");
+
+ await manager.onStartup();
+
+ await manager.enroll(recipe);
+ 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"
+ );
+});
+
+add_task(
+ async function test_setExperimentActive_sendEnrollmentTelemetry_called() {
+ const manager = ExperimentFakes.manager();
+ const sandbox = sinon.createSandbox();
+ sandbox.spy(manager, "setExperimentActive");
+ sandbox.spy(manager, "sendEnrollmentTelemetry");
+
+ await manager.onStartup();
+
+ await manager.onStartup();
+
+ await manager.enroll(ExperimentFakes.recipe("foo"));
+ 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"
+ );
+ }
+);
+
+/**
+ * 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");
+
+ await manager.onStartup();
+
+ // simulate adding a previouly enrolled experiment
+ manager.store.addExperiment(ExperimentFakes.experiment("foo"));
+
+ await Assert.rejects(
+ manager.enroll(ExperimentFakes.recipe("foo")),
+ /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"
+ );
+});
+
+add_task(async function test_failure_group_conflict() {
+ const manager = ExperimentFakes.manager();
+ const sandbox = sinon.createSandbox();
+ sandbox.spy(manager, "sendFailureTelemetry");
+
+ await manager.onStartup();
+
+ // Two conflicting branches that both have the group "pink"
+ // These should not be allowed to exist simultaneously.
+ const existingBranch = {
+ slug: "treatment",
+ feature: { featureId: "pink", enabled: true },
+ };
+ const newBranch = {
+ slug: "treatment",
+ feature: { featureId: "pink", enabled: true },
+ };
+
+ // simulate adding an experiment with a conflicting group "pink"
+ manager.store.addExperiment(
+ ExperimentFakes.experiment("foo", {
+ branch: existingBranch,
+ })
+ );
+
+ // ensure .enroll chooses the special branch with the conflict
+ sandbox.stub(manager, "chooseBranch").returns(newBranch);
+ await Assert.rejects(
+ manager.enroll(ExperimentFakes.recipe("bar", { branches: [newBranch] })),
+ /An experiment with a conflicting feature already exists/,
+ "should throw 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"
+ );
+});
+
+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"
+ );
+
+ sandbox.reset();
+});
+
+add_task(async function enroll_in_reference_aw_experiment() {
+ const SYNC_DATA_PREF = "messaging-system.syncdatastore.data";
+ Services.prefs.clearUserPref(SYNC_DATA_PREF);
+ let dir = await OS.File.getCurrentDirectory();
+ let src = OS.Path.join(dir, "reference_aboutwelcome_experiment_content.json");
+ let bytes = await OS.File.read(src);
+ const decoder = new TextDecoder();
+ const content = JSON.parse(decoder.decode(bytes));
+ // Create two dummy branches with the content from disk
+ const branches = ["treatment-a", "treatment-b"].map(slug => ({
+ slug,
+ ratio: 1,
+ feature: { 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();
+ await manager.onStartup();
+ await manager.enroll(recipe);
+
+ Assert.ok(manager.store.get("reference-aw"), "Successful onboarding");
+ let prefValue = Services.prefs.getStringPref(SYNC_DATA_PREF);
+ 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");
+});