diff options
Diffstat (limited to '')
-rw-r--r-- | toolkit/components/normandy/test/browser/browser_actions_PreferenceRolloutAction.js | 725 |
1 files changed, 725 insertions, 0 deletions
diff --git a/toolkit/components/normandy/test/browser/browser_actions_PreferenceRolloutAction.js b/toolkit/components/normandy/test/browser/browser_actions_PreferenceRolloutAction.js new file mode 100644 index 0000000000..64b60b3483 --- /dev/null +++ b/toolkit/components/normandy/test/browser/browser_actions_PreferenceRolloutAction.js @@ -0,0 +1,725 @@ +"use strict"; + +const { BaseAction } = ChromeUtils.importESModule( + "resource://normandy/actions/BaseAction.sys.mjs" +); +const { PreferenceRolloutAction } = ChromeUtils.importESModule( + "resource://normandy/actions/PreferenceRolloutAction.sys.mjs" +); +const { PreferenceRollouts } = ChromeUtils.importESModule( + "resource://normandy/lib/PreferenceRollouts.sys.mjs" +); +const { NormandyTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/NormandyTestUtils.sys.mjs" +); + +// Test that a simple recipe enrolls as expected +decorate_task( + withStub(TelemetryEnvironment, "setExperimentActive"), + withSendEventSpy(), + PreferenceRollouts.withTestMock(), + async function simple_recipe_enrollment({ + setExperimentActiveStub, + sendEventSpy, + }) { + const recipe = { + id: 1, + arguments: { + slug: "test-rollout", + preferences: [ + { preferenceName: "test.pref1", value: 1 }, + { preferenceName: "test.pref2", value: true }, + { preferenceName: "test.pref3", value: "it works" }, + ], + }, + }; + + const action = new PreferenceRolloutAction(); + await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH); + await action.finalize(); + is(action.lastError, null, "lastError should be null"); + + // rollout prefs are set + is( + Services.prefs.getIntPref("test.pref1"), + 1, + "integer pref should be set" + ); + is( + Services.prefs.getBoolPref("test.pref2"), + true, + "boolean pref should be set" + ); + is( + Services.prefs.getCharPref("test.pref3"), + "it works", + "string pref should be set" + ); + + // start up prefs are set + is( + Services.prefs.getIntPref("app.normandy.startupRolloutPrefs.test.pref1"), + 1, + "integer startup pref should be set" + ); + is( + Services.prefs.getBoolPref("app.normandy.startupRolloutPrefs.test.pref2"), + true, + "boolean startup pref should be set" + ); + is( + Services.prefs.getCharPref("app.normandy.startupRolloutPrefs.test.pref3"), + "it works", + "string startup pref should be set" + ); + + // rollout was stored + let rollouts = await PreferenceRollouts.getAll(); + Assert.deepEqual( + rollouts, + [ + { + slug: "test-rollout", + state: PreferenceRollouts.STATE_ACTIVE, + preferences: [ + { preferenceName: "test.pref1", value: 1, previousValue: null }, + { preferenceName: "test.pref2", value: true, previousValue: null }, + { + preferenceName: "test.pref3", + value: "it works", + previousValue: null, + }, + ], + enrollmentId: rollouts[0].enrollmentId, + }, + ], + "Rollout should be stored in db" + ); + ok( + NormandyTestUtils.isUuid(rollouts[0].enrollmentId), + "Rollout should have a UUID enrollmentId" + ); + + sendEventSpy.assertEvents([ + [ + "enroll", + "preference_rollout", + recipe.arguments.slug, + { enrollmentId: rollouts[0].enrollmentId }, + ], + ]); + ok( + setExperimentActiveStub.calledWithExactly("test-rollout", "active", { + type: "normandy-prefrollout", + enrollmentId: rollouts[0].enrollmentId, + }), + "a telemetry experiment should be activated" + ); + + // Cleanup + Services.prefs.getDefaultBranch("").deleteBranch("test.pref1"); + Services.prefs.getDefaultBranch("").deleteBranch("test.pref2"); + Services.prefs.getDefaultBranch("").deleteBranch("test.pref3"); + } +); + +// Test that an enrollment's values can change, be removed, and be added +decorate_task( + withSendEventSpy(), + PreferenceRollouts.withTestMock(), + async function update_enrollment({ sendEventSpy }) { + // first enrollment + const recipe = { + id: 1, + arguments: { + slug: "test-rollout", + preferences: [ + { preferenceName: "test.pref1", value: 1 }, + { preferenceName: "test.pref2", value: 1 }, + ], + }, + }; + + let action = new PreferenceRolloutAction(); + await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH); + await action.finalize(); + is(action.lastError, null, "lastError should be null"); + + const defaultBranch = Services.prefs.getDefaultBranch(""); + is(defaultBranch.getIntPref("test.pref1"), 1, "pref1 should be set"); + is(defaultBranch.getIntPref("test.pref2"), 1, "pref2 should be set"); + is( + Services.prefs.getIntPref("app.normandy.startupRolloutPrefs.test.pref1"), + 1, + "startup pref1 should be set" + ); + is( + Services.prefs.getIntPref("app.normandy.startupRolloutPrefs.test.pref2"), + 1, + "startup pref2 should be set" + ); + + // update existing enrollment + recipe.arguments.preferences = [ + // pref1 is removed + // pref2's value is updated + { preferenceName: "test.pref2", value: 2 }, + // pref3 is added + { preferenceName: "test.pref3", value: 2 }, + ]; + action = new PreferenceRolloutAction(); + await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH); + await action.finalize(); + is(action.lastError, null, "lastError should be null"); + + /* Todo because of bug 1502410 and bug 1505941 */ + todo_is( + Services.prefs.getPrefType("test.pref1"), + Services.prefs.PREF_INVALID, + "pref1 should be removed" + ); + is(Services.prefs.getIntPref("test.pref2"), 2, "pref2 should be updated"); + is(Services.prefs.getIntPref("test.pref3"), 2, "pref3 should be added"); + + is( + Services.prefs.getPrefType("app.normandy.startupRolloutPrefs.test.pref1"), + Services.prefs.PREF_INVALID, + "startup pref1 should be removed" + ); + is( + Services.prefs.getIntPref("app.normandy.startupRolloutPrefs.test.pref2"), + 2, + "startup pref2 should be updated" + ); + is( + Services.prefs.getIntPref("app.normandy.startupRolloutPrefs.test.pref3"), + 2, + "startup pref3 should be added" + ); + + // rollout in the DB has been updated + const rollouts = await PreferenceRollouts.getAll(); + Assert.deepEqual( + rollouts, + [ + { + slug: "test-rollout", + state: PreferenceRollouts.STATE_ACTIVE, + preferences: [ + { preferenceName: "test.pref2", value: 2, previousValue: null }, + { preferenceName: "test.pref3", value: 2, previousValue: null }, + ], + }, + ], + "Rollout should be updated in db" + ); + + sendEventSpy.assertEvents([ + [ + "enroll", + "preference_rollout", + "test-rollout", + { enrollmentId: rollouts[0].enrollmentId }, + ], + [ + "update", + "preference_rollout", + "test-rollout", + { previousState: "active", enrollmentId: rollouts[0].enrollmentId }, + ], + ]); + + // Cleanup + Services.prefs.getDefaultBranch("").deleteBranch("test.pref1"); + Services.prefs.getDefaultBranch("").deleteBranch("test.pref2"); + Services.prefs.getDefaultBranch("").deleteBranch("test.pref3"); + } +); + +// Test that a graduated rollout can be ungraduated +decorate_task( + withSendEventSpy(), + PreferenceRollouts.withTestMock(), + async function ungraduate_enrollment({ sendEventSpy }) { + Services.prefs.getDefaultBranch("").setIntPref("test.pref", 1); + await PreferenceRollouts.add({ + slug: "test-rollout", + state: PreferenceRollouts.STATE_GRADUATED, + preferences: [ + { preferenceName: "test.pref", value: 1, previousValue: 1 }, + ], + enrollmentId: "test-enrollment-id", + }); + + let recipe = { + id: 1, + arguments: { + slug: "test-rollout", + preferences: [{ preferenceName: "test.pref", value: 2 }], + }, + }; + + const action = new PreferenceRolloutAction(); + await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH); + await action.finalize(); + is(action.lastError, null, "lastError should be null"); + + is(Services.prefs.getIntPref("test.pref"), 2, "pref should be updated"); + is( + Services.prefs.getIntPref("app.normandy.startupRolloutPrefs.test.pref"), + 2, + "startup pref should be set" + ); + + // rollout in the DB has been ungraduated + const rollouts = await PreferenceRollouts.getAll(); + Assert.deepEqual( + rollouts, + [ + { + slug: "test-rollout", + state: PreferenceRollouts.STATE_ACTIVE, + preferences: [ + { preferenceName: "test.pref", value: 2, previousValue: 1 }, + ], + }, + ], + "Rollout should be updated in db" + ); + + sendEventSpy.assertEvents([ + [ + "update", + "preference_rollout", + "test-rollout", + { previousState: "graduated", enrollmentId: "test-enrollment-id" }, + ], + ]); + + // Cleanup + Services.prefs.getDefaultBranch("").deleteBranch("test.pref"); + } +); + +// Test when recipes conflict, only one is applied +decorate_task( + withSendEventSpy(), + PreferenceRollouts.withTestMock(), + async function conflicting_recipes({ sendEventSpy }) { + // create two recipes that each share a pref and have a unique pref. + const recipe1 = { + id: 1, + arguments: { + slug: "test-rollout-1", + preferences: [ + { preferenceName: "test.pref1", value: 1 }, + { preferenceName: "test.pref2", value: 1 }, + ], + }, + }; + const recipe2 = { + id: 2, + arguments: { + slug: "test-rollout-2", + preferences: [ + { preferenceName: "test.pref1", value: 2 }, + { preferenceName: "test.pref3", value: 2 }, + ], + }, + }; + + // running both in the same session + let action = new PreferenceRolloutAction(); + await action.processRecipe(recipe1, BaseAction.suitability.FILTER_MATCH); + await action.processRecipe(recipe2, BaseAction.suitability.FILTER_MATCH); + await action.finalize(); + is(action.lastError, null, "lastError should be null"); + + // running recipe2 in a separate session shouldn't change things + action = new PreferenceRolloutAction(); + await action.processRecipe(recipe2, BaseAction.suitability.FILTER_MATCH); + await action.finalize(); + is(action.lastError, null, "lastError should be null"); + + is( + Services.prefs.getIntPref("test.pref1"), + 1, + "pref1 is set to recipe1's value" + ); + is( + Services.prefs.getIntPref("test.pref2"), + 1, + "pref2 is set to recipe1's value" + ); + is( + Services.prefs.getPrefType("test.pref3"), + Services.prefs.PREF_INVALID, + "pref3 is not set" + ); + + is( + Services.prefs.getIntPref("app.normandy.startupRolloutPrefs.test.pref1"), + 1, + "startup pref1 is set to recipe1's value" + ); + is( + Services.prefs.getIntPref("app.normandy.startupRolloutPrefs.test.pref2"), + 1, + "startup pref2 is set to recipe1's value" + ); + is( + Services.prefs.getPrefType("app.normandy.startupRolloutPrefs.test.pref3"), + Services.prefs.PREF_INVALID, + "startup pref3 is not set" + ); + + // only successful rollout was stored + const rollouts = await PreferenceRollouts.getAll(); + Assert.deepEqual( + rollouts, + [ + { + slug: "test-rollout-1", + state: PreferenceRollouts.STATE_ACTIVE, + preferences: [ + { preferenceName: "test.pref1", value: 1, previousValue: null }, + { preferenceName: "test.pref2", value: 1, previousValue: null }, + ], + enrollmentId: rollouts[0].enrollmentId, + }, + ], + "Only recipe1's rollout should be stored in db" + ); + + sendEventSpy.assertEvents([ + ["enroll", "preference_rollout", recipe1.arguments.slug], + [ + "enrollFailed", + "preference_rollout", + recipe2.arguments.slug, + { reason: "conflict", preference: "test.pref1" }, + ], + [ + "enrollFailed", + "preference_rollout", + recipe2.arguments.slug, + { reason: "conflict", preference: "test.pref1" }, + ], + ]); + + // Cleanup + Services.prefs.getDefaultBranch("").deleteBranch("test.pref1"); + Services.prefs.getDefaultBranch("").deleteBranch("test.pref2"); + Services.prefs.getDefaultBranch("").deleteBranch("test.pref3"); + } +); + +// Test when the wrong value type is given, the recipe is not applied +decorate_task( + withSendEventSpy(), + PreferenceRollouts.withTestMock(), + async function wrong_preference_value({ sendEventSpy }) { + Services.prefs.getDefaultBranch("").setCharPref("test.pref", "not an int"); + const recipe = { + id: 1, + arguments: { + slug: "test-rollout", + preferences: [{ preferenceName: "test.pref", value: 1 }], + }, + }; + + const action = new PreferenceRolloutAction(); + await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH); + await action.finalize(); + is(action.lastError, null, "lastError should be null"); + + is( + Services.prefs.getCharPref("test.pref"), + "not an int", + "the pref should not be modified" + ); + is( + Services.prefs.getPrefType("app.normandy.startupRolloutPrefs.test.pref"), + Services.prefs.PREF_INVALID, + "startup pref is not set" + ); + + Assert.deepEqual( + await PreferenceRollouts.getAll(), + [], + "no rollout is stored in the db" + ); + sendEventSpy.assertEvents([ + [ + "enrollFailed", + "preference_rollout", + recipe.arguments.slug, + { reason: "invalid type", preference: "test.pref" }, + ], + ]); + + // Cleanup + Services.prefs.getDefaultBranch("").deleteBranch("test.pref"); + } +); + +// Test that even when applying a rollout, user prefs are preserved +decorate_task( + PreferenceRollouts.withTestMock(), + async function preserves_user_prefs() { + Services.prefs + .getDefaultBranch("") + .setCharPref("test.pref", "builtin value"); + Services.prefs.setCharPref("test.pref", "user value"); + const recipe = { + id: 1, + arguments: { + slug: "test-rollout", + preferences: [{ preferenceName: "test.pref", value: "rollout value" }], + }, + }; + + const action = new PreferenceRolloutAction(); + await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH); + await action.finalize(); + is(action.lastError, null, "lastError should be null"); + + is( + Services.prefs.getCharPref("test.pref"), + "user value", + "user branch value should be preserved" + ); + is( + Services.prefs.getDefaultBranch("").getCharPref("test.pref"), + "rollout value", + "default branch value should change" + ); + + const rollouts = await PreferenceRollouts.getAll(); + Assert.deepEqual( + rollouts, + [ + { + slug: "test-rollout", + state: PreferenceRollouts.STATE_ACTIVE, + preferences: [ + { + preferenceName: "test.pref", + value: "rollout value", + previousValue: "builtin value", + }, + ], + enrollmentId: rollouts[0].enrollmentId, + }, + ], + "the rollout is added to the db with the correct previous value" + ); + + // Cleanup + Services.prefs.getDefaultBranch("").deleteBranch("test.pref"); + Services.prefs.deleteBranch("test.pref"); + } +); + +// Enrollment works for prefs with only a user branch value, and no default value. +decorate_task( + PreferenceRollouts.withTestMock(), + async function simple_recipe_enrollment() { + const recipe = { + id: 1, + arguments: { + slug: "test-rollout", + preferences: [{ preferenceName: "test.pref", value: 1 }], + }, + }; + + // Set a pref on the user branch only + Services.prefs.setIntPref("test.pref", 2); + + const action = new PreferenceRolloutAction(); + await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH); + await action.finalize(); + is(action.lastError, null, "lastError should be null"); + + is( + Services.prefs.getIntPref("test.pref"), + 2, + "original user branch value still visible" + ); + is( + Services.prefs.getDefaultBranch("").getIntPref("test.pref"), + 1, + "default branch was set" + ); + is( + Services.prefs.getIntPref("app.normandy.startupRolloutPrefs.test.pref"), + 1, + "startup pref is est" + ); + + // Cleanup + Services.prefs.getDefaultBranch("").deleteBranch("test.pref"); + } +); + +// When running a rollout a second time on a pref that doesn't have an existing +// value, the previous value is handled correctly. +decorate_task( + PreferenceRollouts.withTestMock(), + withSendEventSpy(), + async function ({ sendEventSpy }) { + const recipe = { + id: 1, + arguments: { + slug: "test-rollout", + preferences: [{ preferenceName: "test.pref", value: 1 }], + }, + }; + + // run once + let action = new PreferenceRolloutAction(); + await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH); + await action.finalize(); + is(action.lastError, null, "lastError should be null"); + + // run a second time + action = new PreferenceRolloutAction(); + await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH); + await action.finalize(); + is(action.lastError, null, "lastError should be null"); + + const rollouts = await PreferenceRollouts.getAll(); + + Assert.deepEqual( + rollouts, + [ + { + slug: "test-rollout", + state: PreferenceRollouts.STATE_ACTIVE, + preferences: [ + { preferenceName: "test.pref", value: 1, previousValue: null }, + ], + enrollmentId: rollouts[0].enrollmentId, + }, + ], + "the DB should have the correct value stored for previousValue" + ); + + sendEventSpy.assertEvents([ + [ + "enroll", + "preference_rollout", + "test-rollout", + { enrollmentId: rollouts[0].enrollmentId }, + ], + ]); + } +); + +// New rollouts that are no-ops should send errors +decorate_task( + withStub(TelemetryEnvironment, "setExperimentActive"), + withSendEventSpy(), + PreferenceRollouts.withTestMock(), + async function no_op_new_recipe({ setExperimentActiveStub, sendEventSpy }) { + Services.prefs.getDefaultBranch("").setIntPref("test.pref", 1); + + const recipe = { + id: 1, + arguments: { + slug: "test-rollout", + preferences: [{ preferenceName: "test.pref", value: 1 }], + }, + }; + + const action = new PreferenceRolloutAction(); + await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH); + await action.finalize(); + is(action.lastError, null, "lastError should be null"); + + is(Services.prefs.getIntPref("test.pref"), 1, "pref should not change"); + + // start up pref isn't set + is( + Services.prefs.getPrefType("app.normandy.startupRolloutPrefs.test.pref"), + Services.prefs.PREF_INVALID, + "startup pref1 should not be set" + ); + + // rollout was not stored + Assert.deepEqual( + await PreferenceRollouts.getAll(), + [], + "Rollout should not be stored in db" + ); + + sendEventSpy.assertEvents([ + [ + "enrollFailed", + "preference_rollout", + recipe.arguments.slug, + { reason: "would-be-no-op" }, + ], + ]); + Assert.deepEqual( + setExperimentActiveStub.args, + [], + "a telemetry experiment should not be activated" + ); + + // Cleanup + Services.prefs.getDefaultBranch("").deleteBranch("test.pref"); + } +); + +// New rollouts in the graduation set should silently do nothing +decorate_task( + withStub(TelemetryEnvironment, "setExperimentActive"), + withSendEventSpy(), + PreferenceRollouts.withTestMock({ graduationSet: new Set(["test-rollout"]) }), + async function graduationSetNewRecipe({ + setExperimentActiveStub, + sendEventSpy, + }) { + Services.prefs.getDefaultBranch("").setIntPref("test.pref", 1); + + const recipe = { + id: 1, + arguments: { + slug: "test-rollout", + preferences: [{ preferenceName: "test.pref", value: 1 }], + }, + }; + + const action = new PreferenceRolloutAction(); + await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH); + await action.finalize(); + is(action.lastError, null, "lastError should be null"); + + is(Services.prefs.getIntPref("test.pref"), 1, "pref should not change"); + + // start up pref isn't set + is( + Services.prefs.getPrefType("app.normandy.startupRolloutPrefs.test.pref"), + Services.prefs.PREF_INVALID, + "startup pref1 should not be set" + ); + + // rollout was not stored + Assert.deepEqual( + await PreferenceRollouts.getAll(), + [], + "Rollout should not be stored in db" + ); + + sendEventSpy.assertEvents([]); + Assert.deepEqual( + setExperimentActiveStub.args, + [], + "a telemetry experiment should not be activated" + ); + + // Cleanup + Services.prefs.getDefaultBranch("").deleteBranch("test.pref"); + } +); |