tor-browser

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

ASRouterTargeting.test.js (16927B)


      1 import {
      2  ASRouterTargeting,
      3  CachedTargetingGetter,
      4  getSortedMessages,
      5 } from "modules/ASRouterTargeting.sys.mjs";
      6 import { OnboardingMessageProvider } from "modules/OnboardingMessageProvider.sys.mjs";
      7 import { ASRouterPreferences } from "modules/ASRouterPreferences.sys.mjs";
      8 import { GlobalOverrider } from "tests/unit/utils";
      9 
     10 // Note that tests for the ASRouterTargeting environment can be found in
     11 // test/functional/mochitest/browser_asrouter_targeting.js
     12 
     13 describe("#CachedTargetingGetter", () => {
     14  const sixHours = 6 * 60 * 60 * 1000;
     15  let sandbox;
     16  let clock;
     17  let frecentStub;
     18  let topsitesCache;
     19  let globals;
     20  let doesAppNeedPinStub;
     21  let getAddonsByTypesStub;
     22  beforeEach(() => {
     23    sandbox = sinon.createSandbox();
     24    clock = sinon.useFakeTimers();
     25    frecentStub = sandbox.stub(
     26      global.NewTabUtils.activityStreamProvider,
     27      "getTopFrecentSites"
     28    );
     29    topsitesCache = new CachedTargetingGetter("getTopFrecentSites");
     30    globals = new GlobalOverrider();
     31    globals.set(
     32      "TargetingContext",
     33      class {
     34        static combineContexts() {
     35          return sinon.stub();
     36        }
     37 
     38        evalWithDefault() {
     39          return sinon.stub();
     40        }
     41      }
     42    );
     43    doesAppNeedPinStub = sandbox.stub().resolves();
     44    getAddonsByTypesStub = sandbox.stub().resolves();
     45  });
     46 
     47  afterEach(() => {
     48    sandbox.restore();
     49    clock.restore();
     50    globals.restore();
     51  });
     52 
     53  it("should cache allow for optional getter argument", async () => {
     54    let pinCachedGetter = new CachedTargetingGetter(
     55      "doesAppNeedPin",
     56      true,
     57      undefined,
     58      { doesAppNeedPin: doesAppNeedPinStub }
     59    );
     60    // Need to tick forward because Date.now() is stubbed
     61    clock.tick(sixHours);
     62 
     63    await pinCachedGetter.get();
     64    await pinCachedGetter.get();
     65    await pinCachedGetter.get();
     66 
     67    // Called once; cached request
     68    assert.calledOnce(doesAppNeedPinStub);
     69 
     70    // Called with option argument
     71    assert.calledWith(doesAppNeedPinStub, true);
     72 
     73    // Expire and call again
     74    clock.tick(sixHours);
     75    await pinCachedGetter.get();
     76 
     77    // Call goes through
     78    assert.calledTwice(doesAppNeedPinStub);
     79 
     80    let themesCachedGetter = new CachedTargetingGetter(
     81      "getAddonsByTypes",
     82      ["foo"],
     83      undefined,
     84      { getAddonsByTypes: getAddonsByTypesStub }
     85    );
     86 
     87    // Need to tick forward because Date.now() is stubbed
     88    clock.tick(sixHours);
     89 
     90    await themesCachedGetter.get();
     91    await themesCachedGetter.get();
     92    await themesCachedGetter.get();
     93 
     94    // Called once; cached request
     95    assert.calledOnce(getAddonsByTypesStub);
     96 
     97    // Called with option argument
     98    assert.calledWith(getAddonsByTypesStub, ["foo"]);
     99 
    100    // Expire and call again
    101    clock.tick(sixHours);
    102    await themesCachedGetter.get();
    103 
    104    // Call goes through
    105    assert.calledTwice(getAddonsByTypesStub);
    106  });
    107 
    108  it("should only make a request every 6 hours", async () => {
    109    frecentStub.resolves();
    110    clock.tick(sixHours);
    111 
    112    await topsitesCache.get();
    113    await topsitesCache.get();
    114 
    115    assert.calledOnce(
    116      global.NewTabUtils.activityStreamProvider.getTopFrecentSites
    117    );
    118 
    119    clock.tick(sixHours);
    120 
    121    await topsitesCache.get();
    122 
    123    assert.calledTwice(
    124      global.NewTabUtils.activityStreamProvider.getTopFrecentSites
    125    );
    126  });
    127  it("throws when failing getter", async () => {
    128    frecentStub.rejects(new Error("fake error"));
    129    clock.tick(sixHours);
    130 
    131    // assert.throws expect a function as the first parameter, try/catch is a
    132    // workaround
    133    let rejected = false;
    134    try {
    135      await topsitesCache.get();
    136    } catch (e) {
    137      rejected = true;
    138    }
    139 
    140    assert(rejected);
    141  });
    142  describe("sortMessagesByPriority", () => {
    143    it("should sort messages in descending priority order", async () => {
    144      const [m1, m2, m3 = { id: "m3" }] =
    145        await OnboardingMessageProvider.getUntranslatedMessages();
    146      const checkMessageTargetingStub = sandbox
    147        .stub(ASRouterTargeting, "checkMessageTargeting")
    148        .resolves(false);
    149      sandbox.stub(ASRouterTargeting, "isTriggerMatch").resolves(true);
    150 
    151      await ASRouterTargeting.findMatchingMessage({
    152        messages: [
    153          { ...m1, priority: 0 },
    154          { ...m2, priority: 1 },
    155          { ...m3, priority: 2 },
    156        ],
    157        trigger: "testing",
    158      });
    159 
    160      assert.equal(checkMessageTargetingStub.callCount, 3);
    161 
    162      const [arg_m1] = checkMessageTargetingStub.firstCall.args;
    163      assert.equal(arg_m1.id, m3.id);
    164 
    165      const [arg_m2] = checkMessageTargetingStub.secondCall.args;
    166      assert.equal(arg_m2.id, m2.id);
    167 
    168      const [arg_m3] = checkMessageTargetingStub.thirdCall.args;
    169      assert.equal(arg_m3.id, m1.id);
    170    });
    171    it("should sort messages with no priority last", async () => {
    172      const [m1, m2, m3 = { id: "m3" }] =
    173        await OnboardingMessageProvider.getUntranslatedMessages();
    174      const checkMessageTargetingStub = sandbox
    175        .stub(ASRouterTargeting, "checkMessageTargeting")
    176        .resolves(false);
    177      sandbox.stub(ASRouterTargeting, "isTriggerMatch").resolves(true);
    178 
    179      await ASRouterTargeting.findMatchingMessage({
    180        messages: [
    181          { ...m1, priority: 0 },
    182          { ...m2, priority: undefined },
    183          { ...m3, priority: 2 },
    184        ],
    185        trigger: "testing",
    186      });
    187 
    188      assert.equal(checkMessageTargetingStub.callCount, 3);
    189 
    190      const [arg_m1] = checkMessageTargetingStub.firstCall.args;
    191      assert.equal(arg_m1.id, m3.id);
    192 
    193      const [arg_m2] = checkMessageTargetingStub.secondCall.args;
    194      assert.equal(arg_m2.id, m1.id);
    195 
    196      const [arg_m3] = checkMessageTargetingStub.thirdCall.args;
    197      assert.equal(arg_m3.id, m2.id);
    198    });
    199    it("should keep the order of messages with same priority unchanged", async () => {
    200      const [m1, m2, m3 = { id: "m3" }] =
    201        await OnboardingMessageProvider.getUntranslatedMessages();
    202      const checkMessageTargetingStub = sandbox
    203        .stub(ASRouterTargeting, "checkMessageTargeting")
    204        .resolves(false);
    205      sandbox.stub(ASRouterTargeting, "isTriggerMatch").resolves(true);
    206 
    207      await ASRouterTargeting.findMatchingMessage({
    208        messages: [
    209          { ...m1, priority: 2, targeting: undefined, rank: 1 },
    210          { ...m2, priority: undefined, targeting: undefined, rank: 1 },
    211          { ...m3, priority: 2, targeting: undefined, rank: 1 },
    212        ],
    213        trigger: "testing",
    214      });
    215 
    216      assert.equal(checkMessageTargetingStub.callCount, 3);
    217 
    218      const [arg_m1] = checkMessageTargetingStub.firstCall.args;
    219      assert.equal(arg_m1.id, m1.id);
    220 
    221      const [arg_m2] = checkMessageTargetingStub.secondCall.args;
    222      assert.equal(arg_m2.id, m3.id);
    223 
    224      const [arg_m3] = checkMessageTargetingStub.thirdCall.args;
    225      assert.equal(arg_m3.id, m2.id);
    226    });
    227  });
    228 });
    229 describe("#isTriggerMatch", () => {
    230  let trigger;
    231  let message;
    232  beforeEach(() => {
    233    trigger = { id: "openURL" };
    234    message = { id: "openURL" };
    235  });
    236  it("should return false if trigger and candidate ids are different", () => {
    237    trigger.id = "trigger";
    238    message.id = "message";
    239 
    240    assert.isFalse(ASRouterTargeting.isTriggerMatch(trigger, message));
    241    assert.isTrue(
    242      ASRouterTargeting.isTriggerMatch({ id: "foo" }, { id: "foo" })
    243    );
    244  });
    245  it("should return true if the message we check doesn't have trigger params or patterns", () => {
    246    // No params or patterns defined
    247    assert.isTrue(ASRouterTargeting.isTriggerMatch(trigger, message));
    248  });
    249  it("should return false if the trigger does not have params defined", () => {
    250    message.params = {};
    251 
    252    // trigger.param is undefined
    253    assert.isFalse(ASRouterTargeting.isTriggerMatch(trigger, message));
    254  });
    255  it("should return true if message params includes trigger host", () => {
    256    message.params = ["mozilla.org"];
    257    trigger.param = { host: "mozilla.org" };
    258 
    259    assert.isTrue(ASRouterTargeting.isTriggerMatch(trigger, message));
    260  });
    261  it("should return true if message params includes trigger param.type", () => {
    262    message.params = ["ContentBlockingMilestone"];
    263    trigger.param = { type: "ContentBlockingMilestone" };
    264 
    265    assert.isTrue(Boolean(ASRouterTargeting.isTriggerMatch(trigger, message)));
    266  });
    267  it("should return true if message params match trigger mask", () => {
    268    // STATE_BLOCKED_FINGERPRINTING_CONTENT
    269    message.params = [0x00000040];
    270    trigger.param = { type: 538091584 };
    271 
    272    assert.isTrue(Boolean(ASRouterTargeting.isTriggerMatch(trigger, message)));
    273  });
    274 });
    275 describe("ASRouterTargeting", () => {
    276  let evalStub;
    277  let sandbox;
    278  let clock;
    279  let globals;
    280  let fakeTargetingContext;
    281  beforeEach(() => {
    282    sandbox = sinon.createSandbox();
    283    sandbox.replace(ASRouterTargeting, "Environment", {});
    284    clock = sinon.useFakeTimers();
    285    fakeTargetingContext = {
    286      combineContexts: sandbox.stub(),
    287      evalWithDefault: sandbox.stub().resolves(),
    288      setTelemetrySource: sandbox.stub(),
    289    };
    290    globals = new GlobalOverrider();
    291    globals.set(
    292      "TargetingContext",
    293      class {
    294        static combineContexts(...args) {
    295          return fakeTargetingContext.combineContexts.apply(sandbox, args);
    296        }
    297 
    298        setTelemetrySource(id) {
    299          fakeTargetingContext.setTelemetrySource(id);
    300        }
    301 
    302        evalWithDefault(expr) {
    303          return fakeTargetingContext.evalWithDefault(expr);
    304        }
    305      }
    306    );
    307    evalStub = fakeTargetingContext.evalWithDefault;
    308  });
    309  afterEach(() => {
    310    clock.restore();
    311    sandbox.restore();
    312    globals.restore();
    313  });
    314  it("should provide message.id as source", async () => {
    315    await ASRouterTargeting.checkMessageTargeting(
    316      {
    317        id: "message",
    318        targeting: "true",
    319      },
    320      fakeTargetingContext,
    321      sandbox.stub(),
    322      false
    323    );
    324    assert.calledOnce(fakeTargetingContext.evalWithDefault);
    325    assert.include(
    326      fakeTargetingContext.evalWithDefault.firstCall.args[0],
    327      "!isAIWindow"
    328    );
    329    assert.calledWithExactly(
    330      fakeTargetingContext.setTelemetrySource,
    331      "message"
    332    );
    333  });
    334  it("should cache evaluation result", async () => {
    335    evalStub.resolves(true);
    336    let targetingContext = new global.TargetingContext();
    337 
    338    await ASRouterTargeting.checkMessageTargeting(
    339      { targeting: "jexl1" },
    340      targetingContext,
    341      sandbox.stub(),
    342      true
    343    );
    344    await ASRouterTargeting.checkMessageTargeting(
    345      { targeting: "jexl2" },
    346      targetingContext,
    347      sandbox.stub(),
    348      true
    349    );
    350    await ASRouterTargeting.checkMessageTargeting(
    351      { targeting: "jexl1" },
    352      targetingContext,
    353      sandbox.stub(),
    354      true
    355    );
    356 
    357    assert.calledTwice(evalStub);
    358  });
    359  it("should not cache evaluation result", async () => {
    360    evalStub.resolves(true);
    361    let targetingContext = new global.TargetingContext();
    362 
    363    await ASRouterTargeting.checkMessageTargeting(
    364      { targeting: "jexl" },
    365      targetingContext,
    366      sandbox.stub(),
    367      false
    368    );
    369    await ASRouterTargeting.checkMessageTargeting(
    370      { targeting: "jexl" },
    371      targetingContext,
    372      sandbox.stub(),
    373      false
    374    );
    375    await ASRouterTargeting.checkMessageTargeting(
    376      { targeting: "jexl" },
    377      targetingContext,
    378      sandbox.stub(),
    379      false
    380    );
    381 
    382    assert.calledThrice(evalStub);
    383  });
    384  it("should expire cache entries", async () => {
    385    evalStub.resolves(true);
    386    let targetingContext = new global.TargetingContext();
    387 
    388    await ASRouterTargeting.checkMessageTargeting(
    389      { targeting: "jexl" },
    390      targetingContext,
    391      sandbox.stub(),
    392      true
    393    );
    394    await ASRouterTargeting.checkMessageTargeting(
    395      { targeting: "jexl" },
    396      targetingContext,
    397      sandbox.stub(),
    398      true
    399    );
    400    clock.tick(5 * 60 * 1000 + 1);
    401    await ASRouterTargeting.checkMessageTargeting(
    402      { targeting: "jexl" },
    403      targetingContext,
    404      sandbox.stub(),
    405      true
    406    );
    407 
    408    assert.calledTwice(evalStub);
    409  });
    410  it("defaults to Classic-only targeting when no targeting is specified", async () => {
    411    evalStub.resolves(true);
    412    const targetingContext = new global.TargetingContext();
    413    const message = { id: "test-message" };
    414 
    415    await ASRouterTargeting.checkMessageTargeting(
    416      message,
    417      targetingContext,
    418      null,
    419      false
    420    );
    421 
    422    assert.calledOnce(fakeTargetingContext.evalWithDefault);
    423    assert.calledWith(fakeTargetingContext.evalWithDefault, "!isAIWindow");
    424  });
    425  it("blocks messages in AI windows by default via !isAIWindow", async () => {
    426    evalStub.resolves(true);
    427    const targetingContext = new global.TargetingContext();
    428    targetingContext.isAIWindow = false;
    429    const message = { id: "test-message" };
    430 
    431    await ASRouterTargeting.checkMessageTargeting(
    432      message,
    433      targetingContext,
    434      null,
    435      false
    436    );
    437 
    438    assert.calledOnce(fakeTargetingContext.evalWithDefault);
    439    assert.calledWith(fakeTargetingContext.evalWithDefault, "!isAIWindow");
    440  });
    441  it("does not modify targeting that explicitly references isAIWindow", async () => {
    442    evalStub.resolves(true);
    443    const targetingContext = new global.TargetingContext();
    444    targetingContext.isAIWindow = true;
    445    const message = { id: "test-message", targeting: "isAIWindow" };
    446 
    447    await ASRouterTargeting.checkMessageTargeting(
    448      message,
    449      targetingContext,
    450      null,
    451      false
    452    );
    453 
    454    assert.calledOnce(fakeTargetingContext.evalWithDefault);
    455    assert.calledWith(fakeTargetingContext.evalWithDefault, "isAIWindow");
    456  });
    457 
    458  describe("#findMatchingMessage", () => {
    459    let matchStub;
    460    let messages = [
    461      { id: "FOO", targeting: "match" },
    462      { id: "BAR", targeting: "match" },
    463      { id: "BAZ" },
    464    ];
    465    beforeEach(() => {
    466      matchStub = sandbox
    467        .stub(ASRouterTargeting, "_isMessageMatch")
    468        .callsFake(message => message.targeting === "match");
    469    });
    470    it("should return an array of matches if returnAll is true", async () => {
    471      assert.deepEqual(
    472        await ASRouterTargeting.findMatchingMessage({
    473          messages,
    474          returnAll: true,
    475        }),
    476        [
    477          { id: "FOO", targeting: "match" },
    478          { id: "BAR", targeting: "match" },
    479        ]
    480      );
    481    });
    482    it("should return an empty array if no matches were found and returnAll is true", async () => {
    483      matchStub.returns(false);
    484      assert.deepEqual(
    485        await ASRouterTargeting.findMatchingMessage({
    486          messages,
    487          returnAll: true,
    488        }),
    489        []
    490      );
    491    });
    492    it("should return the first match if returnAll is false", async () => {
    493      assert.deepEqual(
    494        await ASRouterTargeting.findMatchingMessage({
    495          messages,
    496        }),
    497        messages[0]
    498      );
    499    });
    500    it("should return null if if no matches were found and returnAll is false", async () => {
    501      matchStub.returns(false);
    502      assert.deepEqual(
    503        await ASRouterTargeting.findMatchingMessage({
    504          messages,
    505        }),
    506        null
    507      );
    508    });
    509  });
    510 });
    511 
    512 /**
    513 * Messages should be sorted in the following order:
    514 * 1. Rank
    515 * 2. Priority
    516 * 3. If the message has targeting
    517 * 4. Order or randomization, depending on input
    518 */
    519 describe("getSortedMessages", () => {
    520  let globals = new GlobalOverrider();
    521  let sandbox;
    522  beforeEach(() => {
    523    globals.set({ ASRouterPreferences });
    524    sandbox = sinon.createSandbox();
    525  });
    526  afterEach(() => {
    527    sandbox.restore();
    528    globals.restore();
    529  });
    530 
    531  /**
    532   * assertSortsCorrectly - Tests to see if an array, when sorted with getSortedMessages,
    533   *                        returns the items in the expected order.
    534   *
    535   * @param {Message[]} expectedOrderArray - The array of messages in its expected order
    536   * @param {{}} options - The options param for getSortedMessages
    537   * @returns
    538   */
    539  function assertSortsCorrectly(expectedOrderArray, options) {
    540    const input = [...expectedOrderArray].reverse();
    541    const result = getSortedMessages(input, options);
    542    const indexes = result.map(message => expectedOrderArray.indexOf(message));
    543    return assert.equal(
    544      indexes.join(","),
    545      [...expectedOrderArray.keys()].join(","),
    546      "Messsages are out of order"
    547    );
    548  }
    549 
    550  it("should sort messages by priority, then by targeting", () => {
    551    assertSortsCorrectly([
    552      { priority: 100, targeting: "isFoo" },
    553      { priority: 100 },
    554      { priority: 99 },
    555      { priority: 1, targeting: "isFoo" },
    556      { priority: 1 },
    557      {},
    558    ]);
    559  });
    560  it("should sort messages by priority, then targeting, then order if ordered param is true", () => {
    561    assertSortsCorrectly(
    562      [
    563        { priority: 100, order: 4 },
    564        { priority: 100, order: 5 },
    565        { priority: 1, order: 3, targeting: "isFoo" },
    566        { priority: 1, order: 0 },
    567        { priority: 1, order: 1 },
    568        { priority: 1, order: 2 },
    569        { order: 0 },
    570      ],
    571      { ordered: true }
    572    );
    573  });
    574 });