summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/test/unit/asrouter/ASRouterTargeting.test.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/test/unit/asrouter/ASRouterTargeting.test.js')
-rw-r--r--browser/components/newtab/test/unit/asrouter/ASRouterTargeting.test.js574
1 files changed, 574 insertions, 0 deletions
diff --git a/browser/components/newtab/test/unit/asrouter/ASRouterTargeting.test.js b/browser/components/newtab/test/unit/asrouter/ASRouterTargeting.test.js
new file mode 100644
index 0000000000..a6e0eea3af
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/ASRouterTargeting.test.js
@@ -0,0 +1,574 @@
+import {
+ ASRouterTargeting,
+ CachedTargetingGetter,
+ getSortedMessages,
+ QueryCache,
+} from "lib/ASRouterTargeting.jsm";
+import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
+import { ASRouterPreferences } from "lib/ASRouterPreferences.jsm";
+import { GlobalOverrider } from "test/unit/utils";
+
+// Note that tests for the ASRouterTargeting environment can be found in
+// test/functional/mochitest/browser_asrouter_targeting.js
+
+describe("#CachedTargetingGetter", () => {
+ const sixHours = 6 * 60 * 60 * 1000;
+ let sandbox;
+ let clock;
+ let frecentStub;
+ let topsitesCache;
+ let globals;
+ let doesAppNeedPinStub;
+ let getAddonsByTypesStub;
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ clock = sinon.useFakeTimers();
+ frecentStub = sandbox.stub(
+ global.NewTabUtils.activityStreamProvider,
+ "getTopFrecentSites"
+ );
+ topsitesCache = new CachedTargetingGetter("getTopFrecentSites");
+ globals = new GlobalOverrider();
+ globals.set(
+ "TargetingContext",
+ class {
+ static combineContexts(...args) {
+ return sinon.stub();
+ }
+
+ evalWithDefault(expr) {
+ return sinon.stub();
+ }
+ }
+ );
+ doesAppNeedPinStub = sandbox.stub().resolves();
+ getAddonsByTypesStub = sandbox.stub().resolves();
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ clock.restore();
+ globals.restore();
+ });
+
+ it("should cache allow for optional getter argument", async () => {
+ let pinCachedGetter = new CachedTargetingGetter(
+ "doesAppNeedPin",
+ true,
+ undefined,
+ { doesAppNeedPin: doesAppNeedPinStub }
+ );
+ // Need to tick forward because Date.now() is stubbed
+ clock.tick(sixHours);
+
+ await pinCachedGetter.get();
+ await pinCachedGetter.get();
+ await pinCachedGetter.get();
+
+ // Called once; cached request
+ assert.calledOnce(doesAppNeedPinStub);
+
+ // Called with option argument
+ assert.calledWith(doesAppNeedPinStub, true);
+
+ // Expire and call again
+ clock.tick(sixHours);
+ await pinCachedGetter.get();
+
+ // Call goes through
+ assert.calledTwice(doesAppNeedPinStub);
+
+ let themesCachedGetter = new CachedTargetingGetter(
+ "getAddonsByTypes",
+ ["foo"],
+ undefined,
+ { getAddonsByTypes: getAddonsByTypesStub }
+ );
+
+ // Need to tick forward because Date.now() is stubbed
+ clock.tick(sixHours);
+
+ await themesCachedGetter.get();
+ await themesCachedGetter.get();
+ await themesCachedGetter.get();
+
+ // Called once; cached request
+ assert.calledOnce(getAddonsByTypesStub);
+
+ // Called with option argument
+ assert.calledWith(getAddonsByTypesStub, ["foo"]);
+
+ // Expire and call again
+ clock.tick(sixHours);
+ await themesCachedGetter.get();
+
+ // Call goes through
+ assert.calledTwice(getAddonsByTypesStub);
+ });
+
+ it("should only make a request every 6 hours", async () => {
+ frecentStub.resolves();
+ clock.tick(sixHours);
+
+ await topsitesCache.get();
+ await topsitesCache.get();
+
+ assert.calledOnce(
+ global.NewTabUtils.activityStreamProvider.getTopFrecentSites
+ );
+
+ clock.tick(sixHours);
+
+ await topsitesCache.get();
+
+ assert.calledTwice(
+ global.NewTabUtils.activityStreamProvider.getTopFrecentSites
+ );
+ });
+ it("throws when failing getter", async () => {
+ frecentStub.rejects(new Error("fake error"));
+ clock.tick(sixHours);
+
+ // assert.throws expect a function as the first parameter, try/catch is a
+ // workaround
+ let rejected = false;
+ try {
+ await topsitesCache.get();
+ } catch (e) {
+ rejected = true;
+ }
+
+ assert(rejected);
+ });
+ describe("sortMessagesByPriority", () => {
+ it("should sort messages in descending priority order", async () => {
+ const [m1, m2, m3 = { id: "m3" }] =
+ await OnboardingMessageProvider.getUntranslatedMessages();
+ const checkMessageTargetingStub = sandbox
+ .stub(ASRouterTargeting, "checkMessageTargeting")
+ .resolves(false);
+ sandbox.stub(ASRouterTargeting, "isTriggerMatch").resolves(true);
+
+ await ASRouterTargeting.findMatchingMessage({
+ messages: [
+ { ...m1, priority: 0 },
+ { ...m2, priority: 1 },
+ { ...m3, priority: 2 },
+ ],
+ trigger: "testing",
+ });
+
+ assert.equal(checkMessageTargetingStub.callCount, 3);
+
+ const [arg_m1] = checkMessageTargetingStub.firstCall.args;
+ assert.equal(arg_m1.id, m3.id);
+
+ const [arg_m2] = checkMessageTargetingStub.secondCall.args;
+ assert.equal(arg_m2.id, m2.id);
+
+ const [arg_m3] = checkMessageTargetingStub.thirdCall.args;
+ assert.equal(arg_m3.id, m1.id);
+ });
+ it("should sort messages with no priority last", async () => {
+ const [m1, m2, m3 = { id: "m3" }] =
+ await OnboardingMessageProvider.getUntranslatedMessages();
+ const checkMessageTargetingStub = sandbox
+ .stub(ASRouterTargeting, "checkMessageTargeting")
+ .resolves(false);
+ sandbox.stub(ASRouterTargeting, "isTriggerMatch").resolves(true);
+
+ await ASRouterTargeting.findMatchingMessage({
+ messages: [
+ { ...m1, priority: 0 },
+ { ...m2, priority: undefined },
+ { ...m3, priority: 2 },
+ ],
+ trigger: "testing",
+ });
+
+ assert.equal(checkMessageTargetingStub.callCount, 3);
+
+ const [arg_m1] = checkMessageTargetingStub.firstCall.args;
+ assert.equal(arg_m1.id, m3.id);
+
+ const [arg_m2] = checkMessageTargetingStub.secondCall.args;
+ assert.equal(arg_m2.id, m1.id);
+
+ const [arg_m3] = checkMessageTargetingStub.thirdCall.args;
+ assert.equal(arg_m3.id, m2.id);
+ });
+ it("should keep the order of messages with same priority unchanged", async () => {
+ const [m1, m2, m3 = { id: "m3" }] =
+ await OnboardingMessageProvider.getUntranslatedMessages();
+ const checkMessageTargetingStub = sandbox
+ .stub(ASRouterTargeting, "checkMessageTargeting")
+ .resolves(false);
+ sandbox.stub(ASRouterTargeting, "isTriggerMatch").resolves(true);
+
+ await ASRouterTargeting.findMatchingMessage({
+ messages: [
+ { ...m1, priority: 2, targeting: undefined, rank: 1 },
+ { ...m2, priority: undefined, targeting: undefined, rank: 1 },
+ { ...m3, priority: 2, targeting: undefined, rank: 1 },
+ ],
+ trigger: "testing",
+ });
+
+ assert.equal(checkMessageTargetingStub.callCount, 3);
+
+ const [arg_m1] = checkMessageTargetingStub.firstCall.args;
+ assert.equal(arg_m1.id, m1.id);
+
+ const [arg_m2] = checkMessageTargetingStub.secondCall.args;
+ assert.equal(arg_m2.id, m3.id);
+
+ const [arg_m3] = checkMessageTargetingStub.thirdCall.args;
+ assert.equal(arg_m3.id, m2.id);
+ });
+ });
+});
+describe("#isTriggerMatch", () => {
+ let trigger;
+ let message;
+ beforeEach(() => {
+ trigger = { id: "openURL" };
+ message = { id: "openURL" };
+ });
+ it("should return false if trigger and candidate ids are different", () => {
+ trigger.id = "trigger";
+ message.id = "message";
+
+ assert.isFalse(ASRouterTargeting.isTriggerMatch(trigger, message));
+ assert.isTrue(
+ ASRouterTargeting.isTriggerMatch({ id: "foo" }, { id: "foo" })
+ );
+ });
+ it("should return true if the message we check doesn't have trigger params or patterns", () => {
+ // No params or patterns defined
+ assert.isTrue(ASRouterTargeting.isTriggerMatch(trigger, message));
+ });
+ it("should return false if the trigger does not have params defined", () => {
+ message.params = {};
+
+ // trigger.param is undefined
+ assert.isFalse(ASRouterTargeting.isTriggerMatch(trigger, message));
+ });
+ it("should return true if message params includes trigger host", () => {
+ message.params = ["mozilla.org"];
+ trigger.param = { host: "mozilla.org" };
+
+ assert.isTrue(ASRouterTargeting.isTriggerMatch(trigger, message));
+ });
+ it("should return true if message params includes trigger param.type", () => {
+ message.params = ["ContentBlockingMilestone"];
+ trigger.param = { type: "ContentBlockingMilestone" };
+
+ assert.isTrue(Boolean(ASRouterTargeting.isTriggerMatch(trigger, message)));
+ });
+ it("should return true if message params match trigger mask", () => {
+ // STATE_BLOCKED_FINGERPRINTING_CONTENT
+ message.params = [0x00000040];
+ trigger.param = { type: 538091584 };
+
+ assert.isTrue(Boolean(ASRouterTargeting.isTriggerMatch(trigger, message)));
+ });
+});
+describe("#CacheListAttachedOAuthClients", () => {
+ const fourHours = 4 * 60 * 60 * 1000;
+ let sandbox;
+ let clock;
+ let fakeFxAccount;
+ let authClientsCache;
+ let globals;
+
+ beforeEach(() => {
+ globals = new GlobalOverrider();
+ sandbox = sinon.createSandbox();
+ clock = sinon.useFakeTimers();
+ fakeFxAccount = {
+ listAttachedOAuthClients: () => {},
+ };
+ globals.set("fxAccounts", fakeFxAccount);
+ authClientsCache = QueryCache.queries.ListAttachedOAuthClients;
+ sandbox
+ .stub(global.fxAccounts, "listAttachedOAuthClients")
+ .returns(Promise.resolve({}));
+ });
+
+ afterEach(() => {
+ authClientsCache.expire();
+ sandbox.restore();
+ clock.restore();
+ });
+
+ it("should only make additional request every 4 hours", async () => {
+ clock.tick(fourHours);
+
+ await authClientsCache.get();
+ assert.calledOnce(global.fxAccounts.listAttachedOAuthClients);
+
+ clock.tick(fourHours);
+ await authClientsCache.get();
+ assert.calledTwice(global.fxAccounts.listAttachedOAuthClients);
+ });
+
+ it("should not make additional request before 4 hours", async () => {
+ clock.tick(fourHours);
+
+ await authClientsCache.get();
+ assert.calledOnce(global.fxAccounts.listAttachedOAuthClients);
+
+ await authClientsCache.get();
+ assert.calledOnce(global.fxAccounts.listAttachedOAuthClients);
+ });
+});
+describe("ASRouterTargeting", () => {
+ let evalStub;
+ let sandbox;
+ let clock;
+ let globals;
+ let fakeTargetingContext;
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ sandbox.replace(ASRouterTargeting, "Environment", {});
+ clock = sinon.useFakeTimers();
+ fakeTargetingContext = {
+ combineContexts: sandbox.stub(),
+ evalWithDefault: sandbox.stub().resolves(),
+ setTelemetrySource: sandbox.stub(),
+ };
+ globals = new GlobalOverrider();
+ globals.set(
+ "TargetingContext",
+ class {
+ static combineContexts(...args) {
+ return fakeTargetingContext.combineContexts.apply(sandbox, args);
+ }
+
+ setTelemetrySource(id) {
+ fakeTargetingContext.setTelemetrySource(id);
+ }
+
+ evalWithDefault(expr) {
+ return fakeTargetingContext.evalWithDefault(expr);
+ }
+ }
+ );
+ evalStub = fakeTargetingContext.evalWithDefault;
+ });
+ afterEach(() => {
+ clock.restore();
+ sandbox.restore();
+ globals.restore();
+ });
+ it("should provide message.id as source", async () => {
+ await ASRouterTargeting.checkMessageTargeting(
+ {
+ id: "message",
+ targeting: "true",
+ },
+ fakeTargetingContext,
+ sandbox.stub(),
+ false
+ );
+ assert.calledOnce(fakeTargetingContext.evalWithDefault);
+ assert.calledWithExactly(fakeTargetingContext.evalWithDefault, "true");
+ assert.calledWithExactly(
+ fakeTargetingContext.setTelemetrySource,
+ "message"
+ );
+ });
+ it("should cache evaluation result", async () => {
+ evalStub.resolves(true);
+ let targetingContext = new global.TargetingContext();
+
+ await ASRouterTargeting.checkMessageTargeting(
+ { targeting: "jexl1" },
+ targetingContext,
+ sandbox.stub(),
+ true
+ );
+ await ASRouterTargeting.checkMessageTargeting(
+ { targeting: "jexl2" },
+ targetingContext,
+ sandbox.stub(),
+ true
+ );
+ await ASRouterTargeting.checkMessageTargeting(
+ { targeting: "jexl1" },
+ targetingContext,
+ sandbox.stub(),
+ true
+ );
+
+ assert.calledTwice(evalStub);
+ });
+ it("should not cache evaluation result", async () => {
+ evalStub.resolves(true);
+ let targetingContext = new global.TargetingContext();
+
+ await ASRouterTargeting.checkMessageTargeting(
+ { targeting: "jexl" },
+ targetingContext,
+ sandbox.stub(),
+ false
+ );
+ await ASRouterTargeting.checkMessageTargeting(
+ { targeting: "jexl" },
+ targetingContext,
+ sandbox.stub(),
+ false
+ );
+ await ASRouterTargeting.checkMessageTargeting(
+ { targeting: "jexl" },
+ targetingContext,
+ sandbox.stub(),
+ false
+ );
+
+ assert.calledThrice(evalStub);
+ });
+ it("should expire cache entries", async () => {
+ evalStub.resolves(true);
+ let targetingContext = new global.TargetingContext();
+
+ await ASRouterTargeting.checkMessageTargeting(
+ { targeting: "jexl" },
+ targetingContext,
+ sandbox.stub(),
+ true
+ );
+ await ASRouterTargeting.checkMessageTargeting(
+ { targeting: "jexl" },
+ targetingContext,
+ sandbox.stub(),
+ true
+ );
+ clock.tick(5 * 60 * 1000 + 1);
+ await ASRouterTargeting.checkMessageTargeting(
+ { targeting: "jexl" },
+ targetingContext,
+ sandbox.stub(),
+ true
+ );
+
+ assert.calledTwice(evalStub);
+ });
+
+ describe("#findMatchingMessage", () => {
+ let matchStub;
+ let messages = [
+ { id: "FOO", targeting: "match" },
+ { id: "BAR", targeting: "match" },
+ { id: "BAZ" },
+ ];
+ beforeEach(() => {
+ matchStub = sandbox
+ .stub(ASRouterTargeting, "_isMessageMatch")
+ .callsFake(message => message.targeting === "match");
+ });
+ it("should return an array of matches if returnAll is true", async () => {
+ assert.deepEqual(
+ await ASRouterTargeting.findMatchingMessage({
+ messages,
+ returnAll: true,
+ }),
+ [
+ { id: "FOO", targeting: "match" },
+ { id: "BAR", targeting: "match" },
+ ]
+ );
+ });
+ it("should return an empty array if no matches were found and returnAll is true", async () => {
+ matchStub.returns(false);
+ assert.deepEqual(
+ await ASRouterTargeting.findMatchingMessage({
+ messages,
+ returnAll: true,
+ }),
+ []
+ );
+ });
+ it("should return the first match if returnAll is false", async () => {
+ assert.deepEqual(
+ await ASRouterTargeting.findMatchingMessage({
+ messages,
+ }),
+ messages[0]
+ );
+ });
+ it("should return null if if no matches were found and returnAll is false", async () => {
+ matchStub.returns(false);
+ assert.deepEqual(
+ await ASRouterTargeting.findMatchingMessage({
+ messages,
+ }),
+ null
+ );
+ });
+ });
+});
+
+/**
+ * Messages should be sorted in the following order:
+ * 1. Rank
+ * 2. Priority
+ * 3. If the message has targeting
+ * 4. Order or randomization, depending on input
+ */
+describe("getSortedMessages", () => {
+ let globals = new GlobalOverrider();
+ let sandbox;
+ beforeEach(() => {
+ globals.set({ ASRouterPreferences });
+ sandbox = sinon.createSandbox();
+ });
+ afterEach(() => {
+ sandbox.restore();
+ globals.restore();
+ });
+
+ /**
+ * assertSortsCorrectly - Tests to see if an array, when sorted with getSortedMessages,
+ * returns the items in the expected order.
+ *
+ * @param {Message[]} expectedOrderArray - The array of messages in its expected order
+ * @param {{}} options - The options param for getSortedMessages
+ * @returns
+ */
+ function assertSortsCorrectly(expectedOrderArray, options) {
+ const input = [...expectedOrderArray].reverse();
+ const result = getSortedMessages(input, options);
+ const indexes = result.map(message => expectedOrderArray.indexOf(message));
+ return assert.equal(
+ indexes.join(","),
+ [...expectedOrderArray.keys()].join(","),
+ "Messsages are out of order"
+ );
+ }
+
+ it("should sort messages by priority, then by targeting", () => {
+ assertSortsCorrectly([
+ { priority: 100, targeting: "isFoo" },
+ { priority: 100 },
+ { priority: 99 },
+ { priority: 1, targeting: "isFoo" },
+ { priority: 1 },
+ {},
+ ]);
+ });
+ it("should sort messages by priority, then targeting, then order if ordered param is true", () => {
+ assertSortsCorrectly(
+ [
+ { priority: 100, order: 4 },
+ { priority: 100, order: 5 },
+ { priority: 1, order: 3, targeting: "isFoo" },
+ { priority: 1, order: 0 },
+ { priority: 1, order: 1 },
+ { priority: 1, order: 2 },
+ { order: 0 },
+ ],
+ { ordered: true }
+ );
+ });
+});