"use strict"; const { ExperimentAPI, _ExperimentFeature: ExperimentFeature } = ChromeUtils.importESModule("resource://nimbus/ExperimentAPI.sys.mjs"); const { JsonSchema } = ChromeUtils.importESModule( "resource://gre/modules/JsonSchema.sys.mjs" ); ChromeUtils.defineLazyGetter(this, "fetchSchema", () => { return fetch("resource://nimbus/schemas/NimbusEnrollment.schema.json", { credentials: "omit", }).then(rsp => rsp.json()); }); const NON_MATCHING_ROLLOUT = Object.freeze( ExperimentFakes.rollout("non-matching-rollout", { branch: { slug: "slug", features: [ { featureId: "aboutwelcome", value: { enabled: false }, }, ], }, }) ); const MATCHING_ROLLOUT = Object.freeze( ExperimentFakes.rollout("matching-rollout", { branch: { slug: "slug", features: [ { featureId: "aboutwelcome", value: { enabled: false }, }, ], }, }) ); const AW_FAKE_MANIFEST = { description: "Different manifest with a special test variable", isEarlyStartup: true, variables: { remoteValue: { type: "boolean", description: "Test value", }, mochitest: { type: "boolean", }, enabled: { type: "boolean", }, }, }; async function setupForExperimentFeature() { const sandbox = sinon.createSandbox(); const manager = ExperimentFakes.manager(); await manager.onStartup(); sandbox.stub(ExperimentAPI, "_store").get(() => manager.store); return { sandbox, manager }; } add_task(async function validSchema() { const validator = new JsonSchema.Validator(await fetchSchema, { shortCircuit: false, }); { const result = validator.validate(NON_MATCHING_ROLLOUT); Assert.ok(result.valid, JSON.stringify(result.errors, undefined, 2)); } { const result = validator.validate(MATCHING_ROLLOUT); Assert.ok(result.valid, JSON.stringify(result.errors, undefined, 2)); } }); add_task(async function readyCallAfterStore_with_remote_value() { let { sandbox, manager } = await setupForExperimentFeature(); let feature = new ExperimentFeature("aboutwelcome"); Assert.ok(feature.getVariable("enabled"), "Feature is true by default"); await manager.store.addEnrollment(MATCHING_ROLLOUT); Assert.ok(!feature.getVariable("enabled"), "Loads value from store"); manager.store._deleteForTests("aboutwelcome"); sandbox.restore(); }); add_task(async function has_sync_value_before_ready() { let { manager } = await setupForExperimentFeature(); let feature = new ExperimentFeature("aboutwelcome", AW_FAKE_MANIFEST); Assert.equal( feature.getVariable("remoteValue"), undefined, "Feature is true by default" ); Services.prefs.setStringPref( "nimbus.syncdefaultsstore.aboutwelcome", JSON.stringify({ ...MATCHING_ROLLOUT, branch: { feature: MATCHING_ROLLOUT.branch.features[0] }, }) ); Services.prefs.setBoolPref( "nimbus.syncdefaultsstore.aboutwelcome.remoteValue", true ); Assert.equal(feature.getVariable("remoteValue"), true, "Sync load from pref"); manager.store._deleteForTests("aboutwelcome"); }); add_task(async function update_remote_defaults_onUpdate() { let { sandbox, manager } = await setupForExperimentFeature(); let feature = new ExperimentFeature("aboutwelcome"); let stub = sandbox.stub(); feature.onUpdate(stub); await manager.store.addEnrollment(MATCHING_ROLLOUT); Assert.ok(stub.called, "update event called"); Assert.equal(stub.callCount, 1, "Called once for remote configs"); Assert.equal(stub.firstCall.args[1], "rollout-updated", "Correct reason"); manager.store._deleteForTests("aboutwelcome"); sandbox.restore(); }); add_task(async function test_features_over_feature() { let { sandbox, manager } = await setupForExperimentFeature(); let feature = new ExperimentFeature("aboutwelcome"); const rollout_features_and_feature = Object.freeze( ExperimentFakes.rollout("matching-rollout", { branch: { slug: "slug", feature: { featureId: "aboutwelcome", value: { enabled: false }, }, features: [ { featureId: "aboutwelcome", value: { enabled: true }, }, ], }, }) ); const rollout_just_feature = Object.freeze( ExperimentFakes.rollout("matching-rollout", { branch: { slug: "slug", feature: { featureId: "aboutwelcome", value: { enabled: false }, }, }, }) ); await manager.store.addEnrollment(rollout_features_and_feature); Assert.ok( feature.getVariable("enabled"), "Should read from the features property over feature" ); manager.store._deleteForTests("aboutwelcome"); manager.store._deleteForTests("matching-rollout"); await manager.store.addEnrollment(rollout_just_feature); Assert.ok( !feature.getVariable("enabled"), "Should read from the feature property when features doesn't exist" ); manager.store._deleteForTests("aboutwelcome"); manager.store._deleteForTests("matching-rollout"); sandbox.restore(); }); add_task(async function update_remote_defaults_readyPromise() { let { sandbox, manager } = await setupForExperimentFeature(); let feature = new ExperimentFeature("aboutwelcome"); let stub = sandbox.stub(); feature.onUpdate(stub); await manager.store.addEnrollment(MATCHING_ROLLOUT); Assert.ok(stub.calledOnce, "Update called after enrollment processed."); Assert.ok( stub.calledWith("featureUpdate:aboutwelcome", "rollout-updated"), "Update called after enrollment processed." ); manager.store._deleteForTests("aboutwelcome"); sandbox.restore(); }); add_task(async function update_remote_defaults_enabled() { let { sandbox, manager } = await setupForExperimentFeature(); let feature = new ExperimentFeature("aboutwelcome"); Assert.equal( feature.getVariable("enabled"), true, "Feature is enabled by manifest.variables.enabled" ); await manager.store.addEnrollment(NON_MATCHING_ROLLOUT); Assert.ok( !feature.getVariable("enabled"), "Feature is disabled by remote configuration" ); manager.store._deleteForTests("aboutwelcome"); sandbox.restore(); }); // If the branch data returned from the store is not modified // this test should not throw add_task(async function test_getVariable_no_mutation() { let { sandbox, manager } = await setupForExperimentFeature(); sandbox.stub(manager.store, "getExperimentForFeature").returns( Cu.cloneInto( { branch: { features: [{ featureId: "aboutwelcome", value: { mochitest: true } }], }, }, {}, { deepFreeze: true } ) ); let feature = new ExperimentFeature("aboutwelcome", AW_FAKE_MANIFEST); Assert.ok(feature.getVariable("mochitest"), "Got back the expected feature"); sandbox.restore(); }); add_task(async function remote_isEarlyStartup_config() { let { manager } = await setupForExperimentFeature(); let rollout = ExperimentFakes.rollout("password-autocomplete", { branch: { slug: "remote-config-isEarlyStartup", features: [ { featureId: "password-autocomplete", enabled: true, value: { remote: true }, isEarlyStartup: true, }, ], }, }); await manager.onStartup(); await manager.store.addEnrollment(rollout); Assert.ok( Services.prefs.prefHasUserValue( "nimbus.syncdefaultsstore.password-autocomplete" ), "Configuration is marked early startup" ); Services.prefs.clearUserPref( "nimbus.syncdefaultsstore.password-autocomplete" ); });