tor-browser

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

MessageLoaderUtils.test.js (14068B)


      1 import { MessageLoaderUtils } from "modules/ASRouter.sys.mjs";
      2 const { STARTPAGE_VERSION } = MessageLoaderUtils;
      3 
      4 const FAKE_OPTIONS = {
      5  storage: {
      6    set() {
      7      return Promise.resolve();
      8    },
      9    get() {
     10      return Promise.resolve();
     11    },
     12  },
     13  dispatchToAS: () => {},
     14 };
     15 const FAKE_RESPONSE_HEADERS = { get() {} };
     16 
     17 describe("MessageLoaderUtils", () => {
     18  let fetchStub;
     19  let clock;
     20  let sandbox;
     21 
     22  beforeEach(() => {
     23    sandbox = sinon.createSandbox();
     24    clock = sinon.useFakeTimers();
     25    fetchStub = sinon.stub(global, "fetch");
     26  });
     27  afterEach(() => {
     28    sandbox.restore();
     29    clock.restore();
     30    fetchStub.restore();
     31  });
     32 
     33  describe("#loadMessagesForProvider", () => {
     34    it("should return messages for a local provider with hardcoded messages", async () => {
     35      const sourceMessage = { id: "foo" };
     36      const provider = {
     37        id: "provider123",
     38        type: "local",
     39        messages: [sourceMessage],
     40      };
     41 
     42      const result = await MessageLoaderUtils.loadMessagesForProvider(
     43        provider,
     44        FAKE_OPTIONS
     45      );
     46 
     47      assert.isArray(result.messages);
     48      // Does the message have the right properties?
     49      const [message] = result.messages;
     50      assert.propertyVal(message, "id", "foo");
     51      assert.propertyVal(message, "provider", "provider123");
     52    });
     53    it("should filter out local messages listed in the `exclude` field", async () => {
     54      const sourceMessage = { id: "foo" };
     55      const provider = {
     56        id: "provider123",
     57        type: "local",
     58        messages: [sourceMessage],
     59        exclude: ["foo"],
     60      };
     61 
     62      const result = await MessageLoaderUtils.loadMessagesForProvider(
     63        provider,
     64        FAKE_OPTIONS
     65      );
     66 
     67      assert.lengthOf(result.messages, 0);
     68    });
     69    it("should return messages for remote provider", async () => {
     70      const sourceMessage = { id: "foo" };
     71      fetchStub.resolves({
     72        ok: true,
     73        status: 200,
     74        json: () => Promise.resolve({ messages: [sourceMessage] }),
     75        headers: FAKE_RESPONSE_HEADERS,
     76      });
     77      const provider = {
     78        id: "provider123",
     79        type: "remote",
     80        url: "https://foo.com",
     81      };
     82 
     83      const result = await MessageLoaderUtils.loadMessagesForProvider(
     84        provider,
     85        FAKE_OPTIONS
     86      );
     87      assert.isArray(result.messages);
     88      // Does the message have the right properties?
     89      const [message] = result.messages;
     90      assert.propertyVal(message, "id", "foo");
     91      assert.propertyVal(message, "provider", "provider123");
     92      assert.propertyVal(message, "provider_url", "https://foo.com");
     93    });
     94    describe("remote provider HTTP codes", () => {
     95      const testMessage = { id: "foo" };
     96      const provider = {
     97        id: "provider123",
     98        type: "remote",
     99        url: "https://foo.com",
    100        updateCycleInMs: 300,
    101      };
    102      const respJson = { messages: [testMessage] };
    103 
    104      function assertReturnsCorrectMessages(actual) {
    105        assert.isArray(actual.messages);
    106        // Does the message have the right properties?
    107        const [message] = actual.messages;
    108        assert.propertyVal(message, "id", testMessage.id);
    109        assert.propertyVal(message, "provider", provider.id);
    110        assert.propertyVal(message, "provider_url", provider.url);
    111      }
    112 
    113      it("should return messages for 200 response", async () => {
    114        fetchStub.resolves({
    115          ok: true,
    116          status: 200,
    117          json: () => Promise.resolve(respJson),
    118          headers: FAKE_RESPONSE_HEADERS,
    119        });
    120        assertReturnsCorrectMessages(
    121          await MessageLoaderUtils.loadMessagesForProvider(
    122            provider,
    123            FAKE_OPTIONS
    124          )
    125        );
    126      });
    127 
    128      it("should return messages for a 302 response with json", async () => {
    129        fetchStub.resolves({
    130          ok: true,
    131          status: 302,
    132          json: () => Promise.resolve(respJson),
    133          headers: FAKE_RESPONSE_HEADERS,
    134        });
    135        assertReturnsCorrectMessages(
    136          await MessageLoaderUtils.loadMessagesForProvider(
    137            provider,
    138            FAKE_OPTIONS
    139          )
    140        );
    141      });
    142 
    143      it("should return an empty array for a 204 response", async () => {
    144        fetchStub.resolves({
    145          ok: true,
    146          status: 204,
    147          json: () => "",
    148          headers: FAKE_RESPONSE_HEADERS,
    149        });
    150        const result = await MessageLoaderUtils.loadMessagesForProvider(
    151          provider,
    152          FAKE_OPTIONS
    153        );
    154        assert.deepEqual(result.messages, []);
    155      });
    156 
    157      it("should return an empty array for a 500 response", async () => {
    158        fetchStub.resolves({
    159          ok: false,
    160          status: 500,
    161          json: () => "",
    162          headers: FAKE_RESPONSE_HEADERS,
    163        });
    164        const result = await MessageLoaderUtils.loadMessagesForProvider(
    165          provider,
    166          FAKE_OPTIONS
    167        );
    168        assert.deepEqual(result.messages, []);
    169      });
    170 
    171      it("should return cached messages for a 304 response", async () => {
    172        clock.tick(302);
    173        const messages = [{ id: "message-1" }, { id: "message-2" }];
    174        const fakeStorage = {
    175          set() {
    176            return Promise.resolve();
    177          },
    178          get() {
    179            return Promise.resolve({
    180              [provider.id]: {
    181                version: STARTPAGE_VERSION,
    182                url: provider.url,
    183                messages,
    184                etag: "etag0987654321",
    185                lastFetched: 1,
    186              },
    187            });
    188          },
    189        };
    190        fetchStub.resolves({
    191          ok: true,
    192          status: 304,
    193          json: () => "",
    194          headers: FAKE_RESPONSE_HEADERS,
    195        });
    196        const result = await MessageLoaderUtils.loadMessagesForProvider(
    197          provider,
    198          { ...FAKE_OPTIONS, storage: fakeStorage }
    199        );
    200        assert.equal(result.messages.length, messages.length);
    201        messages.forEach(message => {
    202          assert.ok(result.messages.find(m => m.id === message.id));
    203        });
    204      });
    205 
    206      it("should return an empty array if json doesn't parse properly", async () => {
    207        fetchStub.resolves({
    208          ok: false,
    209          status: 200,
    210          json: () => "",
    211          headers: FAKE_RESPONSE_HEADERS,
    212        });
    213        const result = await MessageLoaderUtils.loadMessagesForProvider(
    214          provider,
    215          FAKE_OPTIONS
    216        );
    217        assert.deepEqual(result.messages, []);
    218      });
    219 
    220      it("should report response parsing errors with MessageLoaderUtils.reportError", async () => {
    221        const err = {};
    222        sandbox.spy(MessageLoaderUtils, "reportError");
    223        fetchStub.resolves({
    224          ok: true,
    225          status: 200,
    226          json: sandbox.stub().rejects(err),
    227          headers: FAKE_RESPONSE_HEADERS,
    228        });
    229        await MessageLoaderUtils.loadMessagesForProvider(
    230          provider,
    231          FAKE_OPTIONS
    232        );
    233 
    234        assert.calledOnce(MessageLoaderUtils.reportError);
    235        // Report that json parsing failed
    236        assert.calledWith(MessageLoaderUtils.reportError, err);
    237      });
    238 
    239      it("should report missing `messages` with MessageLoaderUtils.reportError", async () => {
    240        sandbox.spy(MessageLoaderUtils, "reportError");
    241        fetchStub.resolves({
    242          ok: true,
    243          status: 200,
    244          json: sandbox.stub().resolves({}),
    245          headers: FAKE_RESPONSE_HEADERS,
    246        });
    247        await MessageLoaderUtils.loadMessagesForProvider(
    248          provider,
    249          FAKE_OPTIONS
    250        );
    251 
    252        assert.calledOnce(MessageLoaderUtils.reportError);
    253        // Report no messages returned
    254        assert.calledWith(
    255          MessageLoaderUtils.reportError,
    256          "No messages returned from https://foo.com."
    257        );
    258      });
    259 
    260      it("should report bad status responses with MessageLoaderUtils.reportError", async () => {
    261        sandbox.spy(MessageLoaderUtils, "reportError");
    262        fetchStub.resolves({
    263          ok: false,
    264          status: 500,
    265          json: sandbox.stub().resolves({}),
    266          headers: FAKE_RESPONSE_HEADERS,
    267        });
    268        await MessageLoaderUtils.loadMessagesForProvider(
    269          provider,
    270          FAKE_OPTIONS
    271        );
    272 
    273        assert.calledOnce(MessageLoaderUtils.reportError);
    274        // Report no messages returned
    275        assert.calledWith(
    276          MessageLoaderUtils.reportError,
    277          "Invalid response status 500 from https://foo.com."
    278        );
    279      });
    280 
    281      it("should return an empty array if the request rejects", async () => {
    282        fetchStub.rejects(new Error("something went wrong"));
    283        const result = await MessageLoaderUtils.loadMessagesForProvider(
    284          provider,
    285          FAKE_OPTIONS
    286        );
    287        assert.deepEqual(result.messages, []);
    288      });
    289    });
    290    describe("remote provider caching", () => {
    291      const provider = {
    292        id: "provider123",
    293        type: "remote",
    294        url: "https://foo.com",
    295        updateCycleInMs: 300,
    296      };
    297 
    298      it("should return cached results if they aren't expired", async () => {
    299        clock.tick(1);
    300        const messages = [{ id: "message-1" }, { id: "message-2" }];
    301        const fakeStorage = {
    302          set() {
    303            return Promise.resolve();
    304          },
    305          get() {
    306            return Promise.resolve({
    307              [provider.id]: {
    308                version: STARTPAGE_VERSION,
    309                url: provider.url,
    310                messages,
    311                etag: "etag0987654321",
    312                lastFetched: Date.now(),
    313              },
    314            });
    315          },
    316        };
    317        const result = await MessageLoaderUtils.loadMessagesForProvider(
    318          provider,
    319          { ...FAKE_OPTIONS, storage: fakeStorage }
    320        );
    321        assert.equal(result.messages.length, messages.length);
    322        messages.forEach(message => {
    323          assert.ok(result.messages.find(m => m.id === message.id));
    324        });
    325      });
    326 
    327      it("should return fetch results if the cache messages are expired", async () => {
    328        clock.tick(302);
    329        const testMessage = { id: "foo" };
    330        const respJson = { messages: [testMessage] };
    331        const fakeStorage = {
    332          set() {
    333            return Promise.resolve();
    334          },
    335          get() {
    336            return Promise.resolve({
    337              [provider.id]: {
    338                version: STARTPAGE_VERSION,
    339                url: provider.url,
    340                messages: [{ id: "message-1" }, { id: "message-2" }],
    341                etag: "etag0987654321",
    342                lastFetched: 1,
    343              },
    344            });
    345          },
    346        };
    347        fetchStub.resolves({
    348          ok: true,
    349          status: 200,
    350          json: () => Promise.resolve(respJson),
    351          headers: FAKE_RESPONSE_HEADERS,
    352        });
    353        const result = await MessageLoaderUtils.loadMessagesForProvider(
    354          provider,
    355          { ...FAKE_OPTIONS, storage: fakeStorage }
    356        );
    357        assert.equal(result.messages.length, 1);
    358        assert.equal(result.messages[0].id, testMessage.id);
    359      });
    360    });
    361    it("should return an empty array for a remote provider with a blank URL without attempting a request", async () => {
    362      const provider = { id: "provider123", type: "remote", url: "" };
    363 
    364      const result = await MessageLoaderUtils.loadMessagesForProvider(
    365        provider,
    366        FAKE_OPTIONS
    367      );
    368 
    369      assert.notCalled(fetchStub);
    370      assert.deepEqual(result.messages, []);
    371    });
    372    it("should return .lastUpdated with the time at which the messages were fetched", async () => {
    373      const sourceMessage = { id: "foo" };
    374      const provider = {
    375        id: "provider123",
    376        type: "remote",
    377        url: "foo.com",
    378      };
    379 
    380      fetchStub.resolves({
    381        ok: true,
    382        status: 200,
    383        json: () =>
    384          new Promise(resolve => {
    385            clock.tick(42);
    386            resolve({ messages: [sourceMessage] });
    387          }),
    388        headers: FAKE_RESPONSE_HEADERS,
    389      });
    390 
    391      const result = await MessageLoaderUtils.loadMessagesForProvider(
    392        provider,
    393        FAKE_OPTIONS
    394      );
    395 
    396      assert.propertyVal(result, "lastUpdated", 42);
    397    });
    398  });
    399 
    400  describe("#shouldProviderUpdate", () => {
    401    it("should return true if the provider does not had a .lastUpdated property", () => {
    402      assert.isTrue(MessageLoaderUtils.shouldProviderUpdate({ id: "foo" }));
    403    });
    404    it("should return false if the provider does not had a .updateCycleInMs property and has a .lastUpdated", () => {
    405      clock.tick(1);
    406      assert.isFalse(
    407        MessageLoaderUtils.shouldProviderUpdate({ id: "foo", lastUpdated: 0 })
    408      );
    409    });
    410    it("should return true if the time since .lastUpdated is greater than .updateCycleInMs", () => {
    411      clock.tick(301);
    412      assert.isTrue(
    413        MessageLoaderUtils.shouldProviderUpdate({
    414          id: "foo",
    415          lastUpdated: 0,
    416          updateCycleInMs: 300,
    417        })
    418      );
    419    });
    420    it("should return false if the time since .lastUpdated is less than .updateCycleInMs", () => {
    421      clock.tick(299);
    422      assert.isFalse(
    423        MessageLoaderUtils.shouldProviderUpdate({
    424          id: "foo",
    425          lastUpdated: 0,
    426          updateCycleInMs: 300,
    427        })
    428      );
    429    });
    430  });
    431 
    432  describe("#cleanupCache", () => {
    433    it("should remove data for providers no longer active", async () => {
    434      const fakeStorage = {
    435        get: sinon.stub().returns(
    436          Promise.resolve({
    437            "id-1": {},
    438            "id-2": {},
    439            "id-3": {},
    440          })
    441        ),
    442        set: sinon.stub().returns(Promise.resolve()),
    443      };
    444      const fakeProviders = [
    445        { id: "id-1", type: "remote" },
    446        { id: "id-3", type: "remote" },
    447      ];
    448 
    449      await MessageLoaderUtils.cleanupCache(fakeProviders, fakeStorage);
    450 
    451      assert.calledOnce(fakeStorage.set);
    452      assert.calledWith(
    453        fakeStorage.set,
    454        MessageLoaderUtils.REMOTE_LOADER_CACHE_KEY,
    455        { "id-1": {}, "id-3": {} }
    456      );
    457    });
    458  });
    459 });