tor-browser

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

PersonalityProviderWorkerClass.test.js (14952B)


      1 import { GlobalOverrider } from "test/unit/utils";
      2 import { PersonalityProviderWorker } from "lib/PersonalityProvider/PersonalityProviderWorkerClass.mjs";
      3 import {
      4  tokenize,
      5  toksToTfIdfVector,
      6 } from "lib/PersonalityProvider/Tokenize.mjs";
      7 import { RecipeExecutor } from "lib/PersonalityProvider/RecipeExecutor.mjs";
      8 import { NmfTextTagger } from "lib/PersonalityProvider/NmfTextTagger.mjs";
      9 import { NaiveBayesTextTagger } from "lib/PersonalityProvider/NaiveBayesTextTagger.mjs";
     10 
     11 describe("Personality Provider Worker Class", () => {
     12  let instance;
     13  let globals;
     14  let sandbox;
     15 
     16  beforeEach(() => {
     17    sandbox = sinon.createSandbox();
     18    globals = new GlobalOverrider();
     19    globals.set("tokenize", tokenize);
     20    globals.set("toksToTfIdfVector", toksToTfIdfVector);
     21    globals.set("NaiveBayesTextTagger", NaiveBayesTextTagger);
     22    globals.set("NmfTextTagger", NmfTextTagger);
     23    globals.set("RecipeExecutor", RecipeExecutor);
     24    instance = new PersonalityProviderWorker();
     25 
     26    // mock the RecipeExecutor
     27    instance.recipeExecutor = {
     28      executeRecipe: (item, recipe) => {
     29        if (recipe === "history_item_builder") {
     30          if (item.title === "fail") {
     31            return null;
     32          }
     33          return {
     34            title: item.title,
     35            score: item.frecency,
     36            type: "history_item",
     37          };
     38        } else if (recipe === "interest_finalizer") {
     39          return {
     40            title: item.title,
     41            score: item.score * 100,
     42            type: "interest_vector",
     43          };
     44        } else if (recipe === "item_to_rank_builder") {
     45          if (item.title === "fail") {
     46            return null;
     47          }
     48          return {
     49            item_title: item.title,
     50            item_score: item.score,
     51            type: "item_to_rank",
     52          };
     53        } else if (recipe === "item_ranker") {
     54          if (item.title === "fail" || item.item_title === "fail") {
     55            return null;
     56          }
     57          return {
     58            title: item.title,
     59            score: item.item_score * item.score,
     60            type: "ranked_item",
     61          };
     62        }
     63        return null;
     64      },
     65      executeCombinerRecipe: (item1, item2, recipe) => {
     66        if (recipe === "interest_combiner") {
     67          if (
     68            item1.title === "combiner_fail" ||
     69            item2.title === "combiner_fail"
     70          ) {
     71            return null;
     72          }
     73          if (item1.type === undefined) {
     74            item1.type = "combined_iv";
     75          }
     76          if (item1.score === undefined) {
     77            item1.score = 0;
     78          }
     79          return { type: item1.type, score: item1.score + item2.score };
     80        }
     81        return null;
     82      },
     83    };
     84 
     85    instance.interestConfig = {
     86      history_item_builder: "history_item_builder",
     87      history_required_fields: ["a", "b", "c"],
     88      interest_finalizer: "interest_finalizer",
     89      item_to_rank_builder: "item_to_rank_builder",
     90      item_ranker: "item_ranker",
     91      interest_combiner: "interest_combiner",
     92    };
     93  });
     94  afterEach(() => {
     95    sinon.restore();
     96    sandbox.restore();
     97    globals.restore();
     98  });
     99  describe("#setBaseAttachmentsURL", () => {
    100    it("should set baseAttachmentsURL", () => {
    101      instance.setBaseAttachmentsURL("url");
    102      assert.equal(instance.baseAttachmentsURL, "url");
    103    });
    104  });
    105  describe("#setInterestConfig", () => {
    106    it("should set interestConfig", () => {
    107      instance.setInterestConfig("config");
    108      assert.equal(instance.interestConfig, "config");
    109    });
    110  });
    111  describe("#setInterestVector", () => {
    112    it("should set interestVector", () => {
    113      instance.setInterestVector("vector");
    114      assert.equal(instance.interestVector, "vector");
    115    });
    116  });
    117  describe("#onSync", async () => {
    118    it("should sync remote settings collection from onSync", async () => {
    119      sinon.stub(instance, "deleteAttachment").resolves();
    120      sinon.stub(instance, "maybeDownloadAttachment").resolves();
    121 
    122      instance.onSync({
    123        data: {
    124          created: ["create-1", "create-2"],
    125          updated: [
    126            { old: "update-old-1", new: "update-new-1" },
    127            { old: "update-old-2", new: "update-new-2" },
    128          ],
    129          deleted: ["delete-2", "delete-1"],
    130        },
    131      });
    132 
    133      assert(instance.maybeDownloadAttachment.withArgs("create-1").calledOnce);
    134      assert(instance.maybeDownloadAttachment.withArgs("create-2").calledOnce);
    135      assert(
    136        instance.maybeDownloadAttachment.withArgs("update-new-1").calledOnce
    137      );
    138      assert(
    139        instance.maybeDownloadAttachment.withArgs("update-new-2").calledOnce
    140      );
    141 
    142      assert(instance.deleteAttachment.withArgs("delete-1").calledOnce);
    143      assert(instance.deleteAttachment.withArgs("delete-2").calledOnce);
    144      assert(instance.deleteAttachment.withArgs("update-old-1").calledOnce);
    145      assert(instance.deleteAttachment.withArgs("update-old-2").calledOnce);
    146    });
    147  });
    148  describe("#maybeDownloadAttachment", () => {
    149    it("should attempt _downloadAttachment three times for maybeDownloadAttachment", async () => {
    150      let existsStub;
    151      let statStub;
    152      let attachmentStub;
    153      sinon.stub(instance, "_downloadAttachment").resolves();
    154      const makeDirStub = globals.sandbox
    155        .stub(global.IOUtils, "makeDirectory")
    156        .resolves();
    157 
    158      existsStub = globals.sandbox
    159        .stub(global.IOUtils, "exists")
    160        .resolves(true);
    161 
    162      statStub = globals.sandbox
    163        .stub(global.IOUtils, "stat")
    164        .resolves({ size: "1" });
    165 
    166      attachmentStub = {
    167        attachment: {
    168          filename: "file",
    169          size: "1",
    170          // This hash matches the hash generated from the empty Uint8Array returned by the IOUtils.read stub.
    171          hash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    172        },
    173      };
    174 
    175      await instance.maybeDownloadAttachment(attachmentStub);
    176      assert.calledWith(makeDirStub, "personality-provider");
    177      assert.calledOnce(existsStub);
    178      assert.calledOnce(statStub);
    179      assert.notCalled(instance._downloadAttachment);
    180 
    181      existsStub.resetHistory();
    182      statStub.resetHistory();
    183      instance._downloadAttachment.resetHistory();
    184 
    185      attachmentStub = {
    186        attachment: {
    187          filename: "file",
    188          size: "2",
    189        },
    190      };
    191 
    192      await instance.maybeDownloadAttachment(attachmentStub);
    193      assert.calledThrice(existsStub);
    194      assert.calledThrice(statStub);
    195      assert.calledThrice(instance._downloadAttachment);
    196 
    197      existsStub.resetHistory();
    198      statStub.resetHistory();
    199      instance._downloadAttachment.resetHistory();
    200 
    201      attachmentStub = {
    202        attachment: {
    203          filename: "file",
    204          size: "1",
    205          // Bogus hash to trigger an update.
    206          hash: "1234",
    207        },
    208      };
    209 
    210      await instance.maybeDownloadAttachment(attachmentStub);
    211      assert.calledThrice(existsStub);
    212      assert.calledThrice(statStub);
    213      assert.calledThrice(instance._downloadAttachment);
    214    });
    215  });
    216  describe("#_downloadAttachment", () => {
    217    beforeEach(() => {
    218      globals.set("Uint8Array", class Uint8Array {});
    219    });
    220    it("should write a file from _downloadAttachment", async () => {
    221      globals.set(
    222        "XMLHttpRequest",
    223        class {
    224          constructor() {
    225            this.status = 200;
    226            this.response = "response!";
    227          }
    228          open() {}
    229          setRequestHeader() {}
    230          send() {}
    231        }
    232      );
    233 
    234      const ioutilsWriteStub = globals.sandbox
    235        .stub(global.IOUtils, "write")
    236        .resolves();
    237 
    238      await instance._downloadAttachment({
    239        attachment: { location: "location", filename: "filename" },
    240      });
    241 
    242      const writeArgs = ioutilsWriteStub.firstCall.args;
    243      assert.equal(writeArgs[0], "filename");
    244      assert.equal(writeArgs[2].tmpPath, "filename.tmp");
    245    });
    246    it("should call console.error from _downloadAttachment if not valid response", async () => {
    247      globals.set(
    248        "XMLHttpRequest",
    249        class {
    250          constructor() {
    251            this.status = 0;
    252            this.response = "response!";
    253          }
    254          open() {}
    255          setRequestHeader() {}
    256          send() {}
    257        }
    258      );
    259 
    260      const consoleErrorStub = globals.sandbox
    261        .stub(console, "error")
    262        .resolves();
    263 
    264      await instance._downloadAttachment({
    265        attachment: { location: "location", filename: "filename" },
    266      });
    267 
    268      assert.calledOnce(consoleErrorStub);
    269    });
    270  });
    271  describe("#deleteAttachment", () => {
    272    it("should remove attachments when calling deleteAttachment", async () => {
    273      const makeDirStub = globals.sandbox
    274        .stub(global.IOUtils, "makeDirectory")
    275        .resolves();
    276      const removeStub = globals.sandbox
    277        .stub(global.IOUtils, "remove")
    278        .resolves();
    279      await instance.deleteAttachment({ attachment: { filename: "filename" } });
    280      assert.calledOnce(makeDirStub);
    281      assert.calledTwice(removeStub);
    282      assert.calledWith(removeStub.firstCall, "filename", {
    283        ignoreAbsent: true,
    284      });
    285      assert.calledWith(removeStub.secondCall, "personality-provider", {
    286        ignoreAbsent: true,
    287      });
    288    });
    289  });
    290  describe("#getAttachment", () => {
    291    it("should return JSON when calling getAttachment", async () => {
    292      sinon.stub(instance, "maybeDownloadAttachment").resolves();
    293      const readJSONStub = globals.sandbox
    294        .stub(global.IOUtils, "readJSON")
    295        .resolves({});
    296      const record = { attachment: { filename: "filename" } };
    297      let returnValue = await instance.getAttachment(record);
    298 
    299      assert.calledOnce(readJSONStub);
    300      assert.calledWith(readJSONStub, "filename");
    301      assert.calledOnce(instance.maybeDownloadAttachment);
    302      assert.calledWith(instance.maybeDownloadAttachment, record);
    303      assert.deepEqual(returnValue, {});
    304 
    305      readJSONStub.restore();
    306      globals.sandbox.stub(global.IOUtils, "readJSON").throws("foo");
    307      const consoleErrorStub = globals.sandbox
    308        .stub(console, "error")
    309        .resolves();
    310      returnValue = await instance.getAttachment(record);
    311 
    312      assert.calledOnce(consoleErrorStub);
    313      assert.deepEqual(returnValue, {});
    314    });
    315  });
    316  describe("#fetchModels", () => {
    317    it("should return ok true", async () => {
    318      sinon.stub(instance, "getAttachment").resolves();
    319      const result = await instance.fetchModels([{ key: 1234 }]);
    320      assert.isTrue(result.ok);
    321      assert.deepEqual(instance.models, [{ recordKey: 1234 }]);
    322    });
    323    it("should return ok false", async () => {
    324      sinon.stub(instance, "getAttachment").resolves();
    325      const result = await instance.fetchModels([]);
    326      assert.isTrue(!result.ok);
    327    });
    328  });
    329  describe("#generateTaggers", () => {
    330    it("should generate taggers from modelKeys", () => {
    331      const modelKeys = ["nb_model_sports", "nmf_model_sports"];
    332 
    333      instance.models = [
    334        { recordKey: "nb_model_sports", model_type: "nb" },
    335        {
    336          recordKey: "nmf_model_sports",
    337          model_type: "nmf",
    338          parent_tag: "nmf_sports_parent_tag",
    339        },
    340      ];
    341 
    342      instance.generateTaggers(modelKeys);
    343      assert.equal(instance.taggers.nbTaggers.length, 1);
    344      assert.equal(Object.keys(instance.taggers.nmfTaggers).length, 1);
    345    });
    346    it("should skip any models not in modelKeys", () => {
    347      const modelKeys = ["nb_model_sports"];
    348 
    349      instance.models = [
    350        { recordKey: "nb_model_sports", model_type: "nb" },
    351        {
    352          recordKey: "nmf_model_sports",
    353          model_type: "nmf",
    354          parent_tag: "nmf_sports_parent_tag",
    355        },
    356      ];
    357 
    358      instance.generateTaggers(modelKeys);
    359      assert.equal(instance.taggers.nbTaggers.length, 1);
    360      assert.equal(Object.keys(instance.taggers.nmfTaggers).length, 0);
    361    });
    362    it("should skip any models not defined", () => {
    363      const modelKeys = ["nb_model_sports", "nmf_model_sports"];
    364 
    365      instance.models = [{ recordKey: "nb_model_sports", model_type: "nb" }];
    366      instance.generateTaggers(modelKeys);
    367      assert.equal(instance.taggers.nbTaggers.length, 1);
    368      assert.equal(Object.keys(instance.taggers.nmfTaggers).length, 0);
    369    });
    370  });
    371  describe("#generateRecipeExecutor", () => {
    372    it("should generate a recipeExecutor", () => {
    373      instance.recipeExecutor = null;
    374      instance.taggers = {};
    375      instance.generateRecipeExecutor();
    376      assert.isNotNull(instance.recipeExecutor);
    377    });
    378  });
    379  describe("#createInterestVector", () => {
    380    let mockHistory = [];
    381    beforeEach(() => {
    382      mockHistory = [
    383        {
    384          title: "automotive",
    385          description: "something about automotive",
    386          url: "http://example.com/automotive",
    387          frecency: 10,
    388        },
    389        {
    390          title: "fashion",
    391          description: "something about fashion",
    392          url: "http://example.com/fashion",
    393          frecency: 5,
    394        },
    395        {
    396          title: "tech",
    397          description: "something about tech",
    398          url: "http://example.com/tech",
    399          frecency: 1,
    400        },
    401      ];
    402    });
    403    it("should gracefully handle history entries that fail", () => {
    404      mockHistory.push({ title: "fail" });
    405      assert.isNotNull(instance.createInterestVector(mockHistory));
    406    });
    407 
    408    it("should fail if the combiner fails", () => {
    409      mockHistory.push({ title: "combiner_fail", frecency: 111 });
    410      let actual = instance.createInterestVector(mockHistory);
    411      assert.isNull(actual);
    412    });
    413 
    414    it("should process history, combine, and finalize", () => {
    415      let actual = instance.createInterestVector(mockHistory);
    416      assert.equal(actual.interestVector.score, 1600);
    417    });
    418  });
    419  describe("#calculateItemRelevanceScore", () => {
    420    it("should return null for busted item", () => {
    421      assert.equal(
    422        instance.calculateItemRelevanceScore({ title: "fail" }),
    423        null
    424      );
    425    });
    426    it("should return null for a busted ranking", () => {
    427      instance.interestVector = { title: "fail", score: 10 };
    428      assert.equal(
    429        instance.calculateItemRelevanceScore({ title: "some item", score: 6 }),
    430        null
    431      );
    432    });
    433    it("should return a score, and not change with interestVector", () => {
    434      instance.interestVector = { score: 10 };
    435      assert.equal(
    436        instance.calculateItemRelevanceScore({ score: 2 }).rankingVector.score,
    437        20
    438      );
    439      assert.deepEqual(instance.interestVector, { score: 10 });
    440    });
    441    it("should use defined personalization_models if available", () => {
    442      instance.interestVector = { score: 10 };
    443      const item = {
    444        score: 2,
    445        personalization_models: {
    446          entertainment: 1,
    447        },
    448      };
    449      assert.equal(
    450        instance.calculateItemRelevanceScore(item).scorableItem.item_tags
    451          .entertainment,
    452        1
    453      );
    454    });
    455  });
    456 });