tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ASRouterPreferences.test.js (17743B)


      1 import {
      2  _ASRouterPreferences,
      3  ASRouterPreferences as ASRouterPreferencesSingleton,
      4  TEST_PROVIDERS,
      5 } from "modules/ASRouterPreferences.sys.mjs";
      6 const FAKE_PROVIDERS = [{ id: "foo" }, { id: "bar" }];
      7 
      8 const PROVIDER_PREF_BRANCH =
      9  "browser.newtabpage.activity-stream.asrouter.providers.";
     10 const DEVTOOLS_PREF =
     11  "browser.newtabpage.activity-stream.asrouter.devtoolsEnabled";
     12 const CFR_USER_PREF_ADDONS =
     13  "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons";
     14 const CFR_USER_PREF_FEATURES =
     15  "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features";
     16 
     17 /**
     18 * NUMBER_OF_PREFS_TO_OBSERVE includes:
     19 *  1. asrouter.providers. pref branch
     20 *  2. asrouter.devtoolsEnabled
     21 *  3. browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons (user preference - cfr)
     22 *  4. browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features (user preference - cfr)
     23 *  5. services.sync.username
     24 */
     25 const NUMBER_OF_PREFS_TO_OBSERVE = 5;
     26 
     27 describe("ASRouterPreferences", () => {
     28  let ASRouterPreferences;
     29  let sandbox;
     30  let addObserverStub;
     31  let stringPrefStub;
     32  let boolPrefStub;
     33  let resetStub;
     34  let hasUserValueStub;
     35  let childListStub;
     36  let setStringPrefStub;
     37 
     38  beforeEach(() => {
     39    ASRouterPreferences = new _ASRouterPreferences();
     40 
     41    sandbox = sinon.createSandbox();
     42    addObserverStub = sandbox.stub(global.Services.prefs, "addObserver");
     43    stringPrefStub = sandbox.stub(global.Services.prefs, "getStringPref");
     44    resetStub = sandbox.stub(global.Services.prefs, "clearUserPref");
     45    setStringPrefStub = sandbox.stub(global.Services.prefs, "setStringPref");
     46    FAKE_PROVIDERS.forEach(provider => {
     47      stringPrefStub
     48        .withArgs(`${PROVIDER_PREF_BRANCH}${provider.id}`)
     49        .returns(JSON.stringify(provider));
     50    });
     51 
     52    boolPrefStub = sandbox
     53      .stub(global.Services.prefs, "getBoolPref")
     54      .returns(false);
     55 
     56    hasUserValueStub = sandbox
     57      .stub(global.Services.prefs, "prefHasUserValue")
     58      .returns(false);
     59 
     60    childListStub = sandbox.stub(global.Services.prefs, "getChildList");
     61    childListStub
     62      .withArgs(PROVIDER_PREF_BRANCH)
     63      .returns(
     64        FAKE_PROVIDERS.map(provider => `${PROVIDER_PREF_BRANCH}${provider.id}`)
     65      );
     66  });
     67 
     68  afterEach(() => {
     69    sandbox.restore();
     70  });
     71 
     72  function getPrefNameForProvider(providerId) {
     73    return `${PROVIDER_PREF_BRANCH}${providerId}`;
     74  }
     75 
     76  function setPrefForProvider(providerId, value) {
     77    stringPrefStub
     78      .withArgs(getPrefNameForProvider(providerId))
     79      .returns(JSON.stringify(value));
     80  }
     81 
     82  it("ASRouterPreferences should be an instance of _ASRouterPreferences", () => {
     83    assert.instanceOf(ASRouterPreferencesSingleton, _ASRouterPreferences);
     84  });
     85  describe("#init", () => {
     86    it("should set ._initialized to true", () => {
     87      ASRouterPreferences.init();
     88      assert.isTrue(ASRouterPreferences._initialized);
     89    });
     90    it("should migrate the provider prefs", () => {
     91      ASRouterPreferences.uninit();
     92      // Should be migrated because they contain bucket and not collection
     93      const MIGRATE_PROVIDERS = [
     94        { id: "baz", bucket: "buk" },
     95        { id: "qux", bucket: "buk" },
     96      ];
     97      // Should be cleared to defaults because it throws on setStringPref
     98      const ERROR_PROVIDER = { id: "err", bucket: "buk" };
     99      // Should not be migrated because, although modified, it lacks bucket
    100      const MODIFIED_SAFE_PROVIDER = { id: "safe" };
    101      const ALL_PROVIDERS = [
    102        ...MIGRATE_PROVIDERS,
    103        ...FAKE_PROVIDERS, // Should not be migrated because they're unmodified
    104        MODIFIED_SAFE_PROVIDER,
    105        ERROR_PROVIDER,
    106      ];
    107      // The migrator should attempt to read prefs for all of these providers
    108      const TRY_PROVIDERS = [
    109        ...MIGRATE_PROVIDERS,
    110        MODIFIED_SAFE_PROVIDER,
    111        ERROR_PROVIDER,
    112      ];
    113 
    114      // Update the full list of provider prefs
    115      childListStub
    116        .withArgs(PROVIDER_PREF_BRANCH)
    117        .returns(
    118          ALL_PROVIDERS.map(provider => getPrefNameForProvider(provider.id))
    119        );
    120      // Stub the pref values so the migrator can read them
    121      ALL_PROVIDERS.forEach(provider => {
    122        stringPrefStub
    123          .withArgs(getPrefNameForProvider(provider.id))
    124          .returns(JSON.stringify(provider));
    125      });
    126 
    127      // Consider these providers' prefs "modified"
    128      TRY_PROVIDERS.forEach(provider => {
    129        hasUserValueStub
    130          .withArgs(`${PROVIDER_PREF_BRANCH}${provider.id}`)
    131          .returns(true);
    132      });
    133      // Spoof an error when trying to set the pref for this provider so we can
    134      // test that the pref is gracefully reset on error
    135      setStringPrefStub
    136        .withArgs(getPrefNameForProvider(ERROR_PROVIDER.id))
    137        .throws();
    138 
    139      ASRouterPreferences.init();
    140 
    141      // The migrator should have tried to check each pref for user modification
    142      ALL_PROVIDERS.forEach(provider =>
    143        assert.calledWith(hasUserValueStub, getPrefNameForProvider(provider.id))
    144      );
    145      // Test that we don't call getStringPref for providers that don't have a
    146      // user-defined value
    147      FAKE_PROVIDERS.forEach(provider =>
    148        assert.neverCalledWith(
    149          stringPrefStub,
    150          getPrefNameForProvider(provider.id)
    151        )
    152      );
    153      // But we do call it for providers that do have a user-defined value
    154      TRY_PROVIDERS.forEach(provider =>
    155        assert.calledWith(stringPrefStub, getPrefNameForProvider(provider.id))
    156      );
    157 
    158      // Test that we don't call setStringPref to migrate providers that don't
    159      // have a bucket property
    160      assert.neverCalledWith(
    161        setStringPrefStub,
    162        getPrefNameForProvider(MODIFIED_SAFE_PROVIDER.id)
    163      );
    164 
    165      /**
    166       * For a given provider, return a sinon matcher that matches if the value
    167       * looks like a migrated version of the original provider. Requires that:
    168       * its id matches the original provider's id; it has no bucket; and its
    169       * collection is set to the value of the original provider's bucket.
    170       *
    171       * @param {object} provider the provider object to compare to
    172       * @returns {object} custom matcher object for sinon
    173       */
    174      function providerJsonMatches(provider) {
    175        return sandbox.match(migrated => {
    176          const parsed = JSON.parse(migrated);
    177          return (
    178            parsed.id === provider.id &&
    179            !("bucket" in parsed) &&
    180            parsed.collection === provider.bucket
    181          );
    182        });
    183      }
    184 
    185      // Test that we call setStringPref to migrate providers that have a bucket
    186      // property and don't have a collection property
    187      MIGRATE_PROVIDERS.forEach(provider =>
    188        assert.calledWith(
    189          setStringPrefStub,
    190          getPrefNameForProvider(provider.id),
    191          providerJsonMatches(provider) // Verify the migrated pref value
    192        )
    193      );
    194 
    195      // Test that we clear the pref for providers that throw when we try to
    196      // read or write them
    197      assert.calledWith(resetStub, getPrefNameForProvider(ERROR_PROVIDER.id));
    198    });
    199    it(`should set ${NUMBER_OF_PREFS_TO_OBSERVE} observers and not re-initialize if already initialized`, () => {
    200      ASRouterPreferences.init();
    201      assert.callCount(addObserverStub, NUMBER_OF_PREFS_TO_OBSERVE);
    202      ASRouterPreferences.init();
    203      ASRouterPreferences.init();
    204      assert.callCount(addObserverStub, NUMBER_OF_PREFS_TO_OBSERVE);
    205    });
    206  });
    207  describe("#uninit", () => {
    208    it("should set ._initialized to false", () => {
    209      ASRouterPreferences.init();
    210      ASRouterPreferences.uninit();
    211      assert.isFalse(ASRouterPreferences._initialized);
    212    });
    213    it("should clear cached values for ._initialized, .devtoolsEnabled", () => {
    214      ASRouterPreferences.init();
    215      // trigger caching
    216      // eslint-disable-next-line no-unused-vars
    217      const result = [
    218        ASRouterPreferences.providers,
    219        ASRouterPreferences.devtoolsEnabled,
    220      ];
    221      assert.isNotNull(
    222        ASRouterPreferences._providers,
    223        "providers should not be null"
    224      );
    225      assert.isNotNull(
    226        ASRouterPreferences._devtoolsEnabled,
    227        "devtolosEnabled should not be null"
    228      );
    229 
    230      ASRouterPreferences.uninit();
    231      assert.isNull(ASRouterPreferences._providers);
    232      assert.isNull(ASRouterPreferences._devtoolsEnabled);
    233    });
    234    it("should clear all listeners and remove observers (only once)", () => {
    235      const removeStub = sandbox.stub(global.Services.prefs, "removeObserver");
    236      ASRouterPreferences.init();
    237      ASRouterPreferences.addListener(() => {});
    238      ASRouterPreferences.addListener(() => {});
    239      assert.equal(ASRouterPreferences._callbacks.size, 2);
    240      ASRouterPreferences.uninit();
    241      // Tests to make sure we don't remove observers that weren't set
    242      ASRouterPreferences.uninit();
    243 
    244      assert.callCount(removeStub, NUMBER_OF_PREFS_TO_OBSERVE);
    245      assert.calledWith(removeStub, PROVIDER_PREF_BRANCH);
    246      assert.calledWith(removeStub, DEVTOOLS_PREF);
    247      assert.isEmpty(ASRouterPreferences._callbacks);
    248    });
    249  });
    250  describe(".providers", () => {
    251    it("should return the value the first time .providers is accessed", () => {
    252      ASRouterPreferences.init();
    253 
    254      const result = ASRouterPreferences.providers;
    255      assert.deepEqual(result, FAKE_PROVIDERS);
    256      // once per pref
    257      assert.calledTwice(stringPrefStub);
    258    });
    259    it("should return the cached value the second time .providers is accessed", () => {
    260      ASRouterPreferences.init();
    261      const [, secondCall] = [
    262        ASRouterPreferences.providers,
    263        ASRouterPreferences.providers,
    264      ];
    265 
    266      assert.deepEqual(secondCall, FAKE_PROVIDERS);
    267      // once per pref
    268      assert.calledTwice(stringPrefStub);
    269    });
    270    it("should just parse the pref each time if ASRouterPreferences hasn't been initialized yet", () => {
    271      // Intentionally not initialized
    272      const [firstCall, secondCall] = [
    273        ASRouterPreferences.providers,
    274        ASRouterPreferences.providers,
    275      ];
    276 
    277      assert.deepEqual(firstCall, FAKE_PROVIDERS);
    278      assert.deepEqual(secondCall, FAKE_PROVIDERS);
    279      assert.callCount(stringPrefStub, 4);
    280    });
    281    it("should skip the pref without throwing if a pref is not parsable", () => {
    282      stringPrefStub.withArgs(`${PROVIDER_PREF_BRANCH}foo`).returns("not json");
    283      ASRouterPreferences.init();
    284 
    285      assert.deepEqual(ASRouterPreferences.providers, [{ id: "bar" }]);
    286    });
    287    it("should include TEST_PROVIDERS if devtools is turned on", () => {
    288      boolPrefStub.withArgs(DEVTOOLS_PREF).returns(true);
    289      ASRouterPreferences.init();
    290 
    291      assert.deepEqual(ASRouterPreferences.providers, [
    292        ...TEST_PROVIDERS,
    293        ...FAKE_PROVIDERS,
    294      ]);
    295    });
    296  });
    297  describe(".devtoolsEnabled", () => {
    298    it("should read the pref the first time .devtoolsEnabled is accessed", () => {
    299      ASRouterPreferences.init();
    300 
    301      const result = ASRouterPreferences.devtoolsEnabled;
    302      assert.deepEqual(result, false);
    303      assert.calledOnce(boolPrefStub);
    304    });
    305    it("should return the cached value the second time .devtoolsEnabled is accessed", () => {
    306      ASRouterPreferences.init();
    307      const [, secondCall] = [
    308        ASRouterPreferences.devtoolsEnabled,
    309        ASRouterPreferences.devtoolsEnabled,
    310      ];
    311 
    312      assert.deepEqual(secondCall, false);
    313      assert.calledOnce(boolPrefStub);
    314    });
    315    it("should just parse the pref each time if ASRouterPreferences hasn't been initialized yet", () => {
    316      // Intentionally not initialized
    317      const [firstCall, secondCall] = [
    318        ASRouterPreferences.devtoolsEnabled,
    319        ASRouterPreferences.devtoolsEnabled,
    320      ];
    321 
    322      assert.deepEqual(firstCall, false);
    323      assert.deepEqual(secondCall, false);
    324      assert.calledTwice(boolPrefStub);
    325    });
    326  });
    327  describe("#getAllUserPreferences", () => {
    328    it("should return all user preferences", () => {
    329      boolPrefStub.withArgs(CFR_USER_PREF_ADDONS).returns(false);
    330      boolPrefStub.withArgs(CFR_USER_PREF_FEATURES).returns(true);
    331      const result = ASRouterPreferences.getAllUserPreferences();
    332      assert.deepEqual(result, {
    333        cfrAddons: false,
    334        cfrFeatures: true,
    335      });
    336    });
    337  });
    338  describe("#enableOrDisableProvider", () => {
    339    it("should enable an existing provider if second param is true", () => {
    340      setPrefForProvider("foo", { id: "foo", enabled: false });
    341      assert.isFalse(ASRouterPreferences.providers[0].enabled);
    342 
    343      ASRouterPreferences.enableOrDisableProvider("foo", true);
    344 
    345      assert.calledWith(
    346        setStringPrefStub,
    347        getPrefNameForProvider("foo"),
    348        JSON.stringify({ id: "foo", enabled: true })
    349      );
    350    });
    351    it("should disable an existing provider if second param is false", () => {
    352      setPrefForProvider("foo", { id: "foo", enabled: true });
    353      assert.isTrue(ASRouterPreferences.providers[0].enabled);
    354 
    355      ASRouterPreferences.enableOrDisableProvider("foo", false);
    356 
    357      assert.calledWith(
    358        setStringPrefStub,
    359        getPrefNameForProvider("foo"),
    360        JSON.stringify({ id: "foo", enabled: false })
    361      );
    362    });
    363    it("should not throw if the id does not exist", () => {
    364      assert.doesNotThrow(() => {
    365        ASRouterPreferences.enableOrDisableProvider("does_not_exist", true);
    366      });
    367    });
    368    it("should not throw if pref is not parseable", () => {
    369      stringPrefStub
    370        .withArgs(getPrefNameForProvider("foo"))
    371        .returns("not valid");
    372      assert.doesNotThrow(() => {
    373        ASRouterPreferences.enableOrDisableProvider("foo", true);
    374      });
    375    });
    376  });
    377  describe("#setUserPreference", () => {
    378    it("should do nothing if the pref doesn't exist", () => {
    379      ASRouterPreferences.setUserPreference("foo", true);
    380      assert.notCalled(boolPrefStub);
    381    });
    382    it("should set the given pref", () => {
    383      const setStub = sandbox.stub(global.Services.prefs, "setBoolPref");
    384      ASRouterPreferences.setUserPreference("cfrAddons", true);
    385      assert.calledWith(setStub, CFR_USER_PREF_ADDONS, true);
    386    });
    387  });
    388  describe("#resetProviderPref", () => {
    389    it("should reset the pref and user prefs", () => {
    390      ASRouterPreferences.resetProviderPref();
    391      FAKE_PROVIDERS.forEach(provider => {
    392        assert.calledWith(resetStub, getPrefNameForProvider(provider.id));
    393      });
    394      assert.calledWith(resetStub, CFR_USER_PREF_ADDONS);
    395      assert.calledWith(resetStub, CFR_USER_PREF_FEATURES);
    396    });
    397  });
    398  describe("observer, listeners", () => {
    399    it("should invalidate .providers when the pref is changed", () => {
    400      const testProvider = { id: "newstuff" };
    401      const newProviders = [...FAKE_PROVIDERS, testProvider];
    402 
    403      ASRouterPreferences.init();
    404 
    405      assert.deepEqual(ASRouterPreferences.providers, FAKE_PROVIDERS);
    406      stringPrefStub
    407        .withArgs(getPrefNameForProvider(testProvider.id))
    408        .returns(JSON.stringify(testProvider));
    409      childListStub
    410        .withArgs(PROVIDER_PREF_BRANCH)
    411        .returns(
    412          newProviders.map(provider => getPrefNameForProvider(provider.id))
    413        );
    414      ASRouterPreferences.observe(
    415        null,
    416        null,
    417        getPrefNameForProvider(testProvider.id)
    418      );
    419 
    420      // Cache should be invalidated so we access the new value of the pref now
    421      assert.deepEqual(ASRouterPreferences.providers, newProviders);
    422    });
    423    it("should invalidate .devtoolsEnabled and .providers when the pref is changed", () => {
    424      ASRouterPreferences.init();
    425 
    426      assert.isFalse(ASRouterPreferences.devtoolsEnabled);
    427      boolPrefStub.withArgs(DEVTOOLS_PREF).returns(true);
    428      childListStub.withArgs(PROVIDER_PREF_BRANCH).returns([]);
    429      ASRouterPreferences.observe(null, null, DEVTOOLS_PREF);
    430 
    431      // Cache should be invalidated so we access the new value of the pref now
    432      // Note that providers needs to be invalidated because devtools adds test content to it.
    433      assert.isTrue(ASRouterPreferences.devtoolsEnabled);
    434      assert.deepEqual(ASRouterPreferences.providers, TEST_PROVIDERS);
    435    });
    436    it("should call listeners added with .addListener", () => {
    437      const callback1 = sinon.stub();
    438      const callback2 = sinon.stub();
    439      ASRouterPreferences.init();
    440      ASRouterPreferences.addListener(callback1);
    441      ASRouterPreferences.addListener(callback2);
    442 
    443      ASRouterPreferences.observe(null, null, getPrefNameForProvider("foo"));
    444      assert.calledWith(callback1, getPrefNameForProvider("foo"));
    445 
    446      ASRouterPreferences.observe(null, null, DEVTOOLS_PREF);
    447      assert.calledWith(callback2, DEVTOOLS_PREF);
    448    });
    449    it("should not call listeners after they are removed with .removeListeners", () => {
    450      const callback = sinon.stub();
    451      ASRouterPreferences.init();
    452      ASRouterPreferences.addListener(callback);
    453 
    454      ASRouterPreferences.observe(null, null, getPrefNameForProvider("foo"));
    455      assert.calledWith(callback, getPrefNameForProvider("foo"));
    456 
    457      callback.reset();
    458      ASRouterPreferences.removeListener(callback);
    459 
    460      ASRouterPreferences.observe(null, null, DEVTOOLS_PREF);
    461      assert.notCalled(callback);
    462    });
    463  });
    464  describe("#_transformPersonalizedCfrScores", () => {
    465    it("should report JSON.parse errors", () => {
    466      sandbox.stub(global.console, "error");
    467 
    468      ASRouterPreferences._transformPersonalizedCfrScores("");
    469 
    470      assert.calledOnce(global.console.error);
    471    });
    472    it("should return an object parsed from a string", () => {
    473      const scores = { FOO: 3000, BAR: 4000 };
    474      assert.deepEqual(
    475        ASRouterPreferences._transformPersonalizedCfrScores(
    476          JSON.stringify(scores)
    477        ),
    478        scores
    479      );
    480    });
    481  });
    482 });