import { _ASRouterPreferences, ASRouterPreferences as ASRouterPreferencesSingleton, TEST_PROVIDERS, } from "modules/ASRouterPreferences.sys.mjs"; const FAKE_PROVIDERS = [{ id: "foo" }, { id: "bar" }]; const PROVIDER_PREF_BRANCH = "browser.newtabpage.activity-stream.asrouter.providers."; const DEVTOOLS_PREF = "browser.newtabpage.activity-stream.asrouter.devtoolsEnabled"; const CFR_USER_PREF_ADDONS = "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons"; const CFR_USER_PREF_FEATURES = "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features"; /** NUMBER_OF_PREFS_TO_OBSERVE includes: * 1. asrouter.providers. pref branch * 2. asrouter.devtoolsEnabled * 3. browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons (user preference - cfr) * 4. browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features (user preference - cfr) * 5. services.sync.username */ const NUMBER_OF_PREFS_TO_OBSERVE = 5; describe("ASRouterPreferences", () => { let ASRouterPreferences; let sandbox; let addObserverStub; let stringPrefStub; let boolPrefStub; let resetStub; let hasUserValueStub; let childListStub; let setStringPrefStub; beforeEach(() => { ASRouterPreferences = new _ASRouterPreferences(); sandbox = sinon.createSandbox(); addObserverStub = sandbox.stub(global.Services.prefs, "addObserver"); stringPrefStub = sandbox.stub(global.Services.prefs, "getStringPref"); resetStub = sandbox.stub(global.Services.prefs, "clearUserPref"); setStringPrefStub = sandbox.stub(global.Services.prefs, "setStringPref"); FAKE_PROVIDERS.forEach(provider => { stringPrefStub .withArgs(`${PROVIDER_PREF_BRANCH}${provider.id}`) .returns(JSON.stringify(provider)); }); boolPrefStub = sandbox .stub(global.Services.prefs, "getBoolPref") .returns(false); hasUserValueStub = sandbox .stub(global.Services.prefs, "prefHasUserValue") .returns(false); childListStub = sandbox.stub(global.Services.prefs, "getChildList"); childListStub .withArgs(PROVIDER_PREF_BRANCH) .returns( FAKE_PROVIDERS.map(provider => `${PROVIDER_PREF_BRANCH}${provider.id}`) ); }); afterEach(() => { sandbox.restore(); }); function getPrefNameForProvider(providerId) { return `${PROVIDER_PREF_BRANCH}${providerId}`; } function setPrefForProvider(providerId, value) { stringPrefStub .withArgs(getPrefNameForProvider(providerId)) .returns(JSON.stringify(value)); } it("ASRouterPreferences should be an instance of _ASRouterPreferences", () => { assert.instanceOf(ASRouterPreferencesSingleton, _ASRouterPreferences); }); describe("#init", () => { it("should set ._initialized to true", () => { ASRouterPreferences.init(); assert.isTrue(ASRouterPreferences._initialized); }); it("should migrate the provider prefs", () => { ASRouterPreferences.uninit(); // Should be migrated because they contain bucket and not collection const MIGRATE_PROVIDERS = [ { id: "baz", bucket: "buk" }, { id: "qux", bucket: "buk" }, ]; // Should be cleared to defaults because it throws on setStringPref const ERROR_PROVIDER = { id: "err", bucket: "buk" }; // Should not be migrated because, although modified, it lacks bucket const MODIFIED_SAFE_PROVIDER = { id: "safe" }; const ALL_PROVIDERS = [ ...MIGRATE_PROVIDERS, ...FAKE_PROVIDERS, // Should not be migrated because they're unmodified MODIFIED_SAFE_PROVIDER, ERROR_PROVIDER, ]; // The migrator should attempt to read prefs for all of these providers const TRY_PROVIDERS = [ ...MIGRATE_PROVIDERS, MODIFIED_SAFE_PROVIDER, ERROR_PROVIDER, ]; // Update the full list of provider prefs childListStub .withArgs(PROVIDER_PREF_BRANCH) .returns( ALL_PROVIDERS.map(provider => getPrefNameForProvider(provider.id)) ); // Stub the pref values so the migrator can read them ALL_PROVIDERS.forEach(provider => { stringPrefStub .withArgs(getPrefNameForProvider(provider.id)) .returns(JSON.stringify(provider)); }); // Consider these providers' prefs "modified" TRY_PROVIDERS.forEach(provider => { hasUserValueStub .withArgs(`${PROVIDER_PREF_BRANCH}${provider.id}`) .returns(true); }); // Spoof an error when trying to set the pref for this provider so we can // test that the pref is gracefully reset on error setStringPrefStub .withArgs(getPrefNameForProvider(ERROR_PROVIDER.id)) .throws(); ASRouterPreferences.init(); // The migrator should have tried to check each pref for user modification ALL_PROVIDERS.forEach(provider => assert.calledWith(hasUserValueStub, getPrefNameForProvider(provider.id)) ); // Test that we don't call getStringPref for providers that don't have a // user-defined value FAKE_PROVIDERS.forEach(provider => assert.neverCalledWith( stringPrefStub, getPrefNameForProvider(provider.id) ) ); // But we do call it for providers that do have a user-defined value TRY_PROVIDERS.forEach(provider => assert.calledWith(stringPrefStub, getPrefNameForProvider(provider.id)) ); // Test that we don't call setStringPref to migrate providers that don't // have a bucket property assert.neverCalledWith( setStringPrefStub, getPrefNameForProvider(MODIFIED_SAFE_PROVIDER.id) ); /** * For a given provider, return a sinon matcher that matches if the value * looks like a migrated version of the original provider. Requires that: * its id matches the original provider's id; it has no bucket; and its * collection is set to the value of the original provider's bucket. * @param {object} provider the provider object to compare to * @returns {object} custom matcher object for sinon */ function providerJsonMatches(provider) { return sandbox.match(migrated => { const parsed = JSON.parse(migrated); return ( parsed.id === provider.id && !("bucket" in parsed) && parsed.collection === provider.bucket ); }); } // Test that we call setStringPref to migrate providers that have a bucket // property and don't have a collection property MIGRATE_PROVIDERS.forEach(provider => assert.calledWith( setStringPrefStub, getPrefNameForProvider(provider.id), providerJsonMatches(provider) // Verify the migrated pref value ) ); // Test that we clear the pref for providers that throw when we try to // read or write them assert.calledWith(resetStub, getPrefNameForProvider(ERROR_PROVIDER.id)); }); it(`should set ${NUMBER_OF_PREFS_TO_OBSERVE} observers and not re-initialize if already initialized`, () => { ASRouterPreferences.init(); assert.callCount(addObserverStub, NUMBER_OF_PREFS_TO_OBSERVE); ASRouterPreferences.init(); ASRouterPreferences.init(); assert.callCount(addObserverStub, NUMBER_OF_PREFS_TO_OBSERVE); }); }); describe("#uninit", () => { it("should set ._initialized to false", () => { ASRouterPreferences.init(); ASRouterPreferences.uninit(); assert.isFalse(ASRouterPreferences._initialized); }); it("should clear cached values for ._initialized, .devtoolsEnabled", () => { ASRouterPreferences.init(); // trigger caching // eslint-disable-next-line no-unused-vars const result = [ ASRouterPreferences.providers, ASRouterPreferences.devtoolsEnabled, ]; assert.isNotNull( ASRouterPreferences._providers, "providers should not be null" ); assert.isNotNull( ASRouterPreferences._devtoolsEnabled, "devtolosEnabled should not be null" ); ASRouterPreferences.uninit(); assert.isNull(ASRouterPreferences._providers); assert.isNull(ASRouterPreferences._devtoolsEnabled); }); it("should clear all listeners and remove observers (only once)", () => { const removeStub = sandbox.stub(global.Services.prefs, "removeObserver"); ASRouterPreferences.init(); ASRouterPreferences.addListener(() => {}); ASRouterPreferences.addListener(() => {}); assert.equal(ASRouterPreferences._callbacks.size, 2); ASRouterPreferences.uninit(); // Tests to make sure we don't remove observers that weren't set ASRouterPreferences.uninit(); assert.callCount(removeStub, NUMBER_OF_PREFS_TO_OBSERVE); assert.calledWith(removeStub, PROVIDER_PREF_BRANCH); assert.calledWith(removeStub, DEVTOOLS_PREF); assert.isEmpty(ASRouterPreferences._callbacks); }); }); describe(".providers", () => { it("should return the value the first time .providers is accessed", () => { ASRouterPreferences.init(); const result = ASRouterPreferences.providers; assert.deepEqual(result, FAKE_PROVIDERS); // once per pref assert.calledTwice(stringPrefStub); }); it("should return the cached value the second time .providers is accessed", () => { ASRouterPreferences.init(); const [, secondCall] = [ ASRouterPreferences.providers, ASRouterPreferences.providers, ]; assert.deepEqual(secondCall, FAKE_PROVIDERS); // once per pref assert.calledTwice(stringPrefStub); }); it("should just parse the pref each time if ASRouterPreferences hasn't been initialized yet", () => { // Intentionally not initialized const [firstCall, secondCall] = [ ASRouterPreferences.providers, ASRouterPreferences.providers, ]; assert.deepEqual(firstCall, FAKE_PROVIDERS); assert.deepEqual(secondCall, FAKE_PROVIDERS); assert.callCount(stringPrefStub, 4); }); it("should skip the pref without throwing if a pref is not parsable", () => { stringPrefStub.withArgs(`${PROVIDER_PREF_BRANCH}foo`).returns("not json"); ASRouterPreferences.init(); assert.deepEqual(ASRouterPreferences.providers, [{ id: "bar" }]); }); it("should include TEST_PROVIDERS if devtools is turned on", () => { boolPrefStub.withArgs(DEVTOOLS_PREF).returns(true); ASRouterPreferences.init(); assert.deepEqual(ASRouterPreferences.providers, [ ...TEST_PROVIDERS, ...FAKE_PROVIDERS, ]); }); }); describe(".devtoolsEnabled", () => { it("should read the pref the first time .devtoolsEnabled is accessed", () => { ASRouterPreferences.init(); const result = ASRouterPreferences.devtoolsEnabled; assert.deepEqual(result, false); assert.calledOnce(boolPrefStub); }); it("should return the cached value the second time .devtoolsEnabled is accessed", () => { ASRouterPreferences.init(); const [, secondCall] = [ ASRouterPreferences.devtoolsEnabled, ASRouterPreferences.devtoolsEnabled, ]; assert.deepEqual(secondCall, false); assert.calledOnce(boolPrefStub); }); it("should just parse the pref each time if ASRouterPreferences hasn't been initialized yet", () => { // Intentionally not initialized const [firstCall, secondCall] = [ ASRouterPreferences.devtoolsEnabled, ASRouterPreferences.devtoolsEnabled, ]; assert.deepEqual(firstCall, false); assert.deepEqual(secondCall, false); assert.calledTwice(boolPrefStub); }); }); describe("#getAllUserPreferences", () => { it("should return all user preferences", () => { boolPrefStub.withArgs(CFR_USER_PREF_ADDONS).returns(false); boolPrefStub.withArgs(CFR_USER_PREF_FEATURES).returns(true); const result = ASRouterPreferences.getAllUserPreferences(); assert.deepEqual(result, { cfrAddons: false, cfrFeatures: true, }); }); }); describe("#enableOrDisableProvider", () => { it("should enable an existing provider if second param is true", () => { setPrefForProvider("foo", { id: "foo", enabled: false }); assert.isFalse(ASRouterPreferences.providers[0].enabled); ASRouterPreferences.enableOrDisableProvider("foo", true); assert.calledWith( setStringPrefStub, getPrefNameForProvider("foo"), JSON.stringify({ id: "foo", enabled: true }) ); }); it("should disable an existing provider if second param is false", () => { setPrefForProvider("foo", { id: "foo", enabled: true }); assert.isTrue(ASRouterPreferences.providers[0].enabled); ASRouterPreferences.enableOrDisableProvider("foo", false); assert.calledWith( setStringPrefStub, getPrefNameForProvider("foo"), JSON.stringify({ id: "foo", enabled: false }) ); }); it("should not throw if the id does not exist", () => { assert.doesNotThrow(() => { ASRouterPreferences.enableOrDisableProvider("does_not_exist", true); }); }); it("should not throw if pref is not parseable", () => { stringPrefStub .withArgs(getPrefNameForProvider("foo")) .returns("not valid"); assert.doesNotThrow(() => { ASRouterPreferences.enableOrDisableProvider("foo", true); }); }); }); describe("#setUserPreference", () => { it("should do nothing if the pref doesn't exist", () => { ASRouterPreferences.setUserPreference("foo", true); assert.notCalled(boolPrefStub); }); it("should set the given pref", () => { const setStub = sandbox.stub(global.Services.prefs, "setBoolPref"); ASRouterPreferences.setUserPreference("cfrAddons", true); assert.calledWith(setStub, CFR_USER_PREF_ADDONS, true); }); }); describe("#resetProviderPref", () => { it("should reset the pref and user prefs", () => { ASRouterPreferences.resetProviderPref(); FAKE_PROVIDERS.forEach(provider => { assert.calledWith(resetStub, getPrefNameForProvider(provider.id)); }); assert.calledWith(resetStub, CFR_USER_PREF_ADDONS); assert.calledWith(resetStub, CFR_USER_PREF_FEATURES); }); }); describe("observer, listeners", () => { it("should invalidate .providers when the pref is changed", () => { const testProvider = { id: "newstuff" }; const newProviders = [...FAKE_PROVIDERS, testProvider]; ASRouterPreferences.init(); assert.deepEqual(ASRouterPreferences.providers, FAKE_PROVIDERS); stringPrefStub .withArgs(getPrefNameForProvider(testProvider.id)) .returns(JSON.stringify(testProvider)); childListStub .withArgs(PROVIDER_PREF_BRANCH) .returns( newProviders.map(provider => getPrefNameForProvider(provider.id)) ); ASRouterPreferences.observe( null, null, getPrefNameForProvider(testProvider.id) ); // Cache should be invalidated so we access the new value of the pref now assert.deepEqual(ASRouterPreferences.providers, newProviders); }); it("should invalidate .devtoolsEnabled and .providers when the pref is changed", () => { ASRouterPreferences.init(); assert.isFalse(ASRouterPreferences.devtoolsEnabled); boolPrefStub.withArgs(DEVTOOLS_PREF).returns(true); childListStub.withArgs(PROVIDER_PREF_BRANCH).returns([]); ASRouterPreferences.observe(null, null, DEVTOOLS_PREF); // Cache should be invalidated so we access the new value of the pref now // Note that providers needs to be invalidated because devtools adds test content to it. assert.isTrue(ASRouterPreferences.devtoolsEnabled); assert.deepEqual(ASRouterPreferences.providers, TEST_PROVIDERS); }); it("should call listeners added with .addListener", () => { const callback1 = sinon.stub(); const callback2 = sinon.stub(); ASRouterPreferences.init(); ASRouterPreferences.addListener(callback1); ASRouterPreferences.addListener(callback2); ASRouterPreferences.observe(null, null, getPrefNameForProvider("foo")); assert.calledWith(callback1, getPrefNameForProvider("foo")); ASRouterPreferences.observe(null, null, DEVTOOLS_PREF); assert.calledWith(callback2, DEVTOOLS_PREF); }); it("should not call listeners after they are removed with .removeListeners", () => { const callback = sinon.stub(); ASRouterPreferences.init(); ASRouterPreferences.addListener(callback); ASRouterPreferences.observe(null, null, getPrefNameForProvider("foo")); assert.calledWith(callback, getPrefNameForProvider("foo")); callback.reset(); ASRouterPreferences.removeListener(callback); ASRouterPreferences.observe(null, null, DEVTOOLS_PREF); assert.notCalled(callback); }); }); describe("#_transformPersonalizedCfrScores", () => { it("should report JSON.parse errors", () => { sandbox.stub(global.console, "error"); ASRouterPreferences._transformPersonalizedCfrScores(""); assert.calledOnce(global.console.error); }); it("should return an object parsed from a string", () => { const scores = { FOO: 3000, BAR: 4000 }; assert.deepEqual( ASRouterPreferences._transformPersonalizedCfrScores( JSON.stringify(scores) ), scores ); }); }); });