tor-browser

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

test_commands.js (19881B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 const { FxAccountsCommands, SendTab } = ChromeUtils.importESModule(
      7  "resource://gre/modules/FxAccountsCommands.sys.mjs"
      8 );
      9 
     10 const { FxAccountsClient } = ChromeUtils.importESModule(
     11  "resource://gre/modules/FxAccountsClient.sys.mjs"
     12 );
     13 
     14 const { COMMAND_SENDTAB, COMMAND_SENDTAB_TAIL } = ChromeUtils.importESModule(
     15  "resource://gre/modules/FxAccountsCommon.sys.mjs"
     16 );
     17 
     18 class TelemetryMock {
     19  constructor() {
     20    this._events = [];
     21    this._uuid_counter = 0;
     22  }
     23 
     24  recordEvent(object, method, value, extra = undefined) {
     25    this._events.push({ object, method, value, extra });
     26  }
     27 
     28  generateFlowID() {
     29    this._uuid_counter += 1;
     30    return this._uuid_counter.toString();
     31  }
     32 
     33  sanitizeDeviceId(id) {
     34    return id + "-san";
     35  }
     36 }
     37 
     38 function FxaInternalMock() {
     39  return {
     40    telemetry: new TelemetryMock(),
     41  };
     42 }
     43 
     44 function MockFxAccountsClient() {
     45  FxAccountsClient.apply(this);
     46 }
     47 
     48 MockFxAccountsClient.prototype = {};
     49 Object.setPrototypeOf(
     50  MockFxAccountsClient.prototype,
     51  FxAccountsClient.prototype
     52 );
     53 
     54 add_setup(function () {
     55  do_get_profile(); // FOG requires a profile dir.
     56  Services.fog.initializeFOG();
     57 });
     58 
     59 add_task(async function test_sendtab_isDeviceCompatible() {
     60  const sendTab = new SendTab(null, null);
     61  let device = { name: "My device" };
     62  Assert.ok(!sendTab.isDeviceCompatible(device));
     63  device = { name: "My device", availableCommands: {} };
     64  Assert.ok(!sendTab.isDeviceCompatible(device));
     65  device = {
     66    name: "My device",
     67    availableCommands: {
     68      "https://identity.mozilla.com/cmd/open-uri": "payload",
     69    },
     70  };
     71  Assert.ok(sendTab.isDeviceCompatible(device));
     72 });
     73 
     74 add_task(async function test_sendtab_send() {
     75  // Clear events from other test cases.
     76  Services.fog.testResetFOG();
     77 
     78  const commands = {
     79    invoke: sinon.spy((cmd, device, payload) => {
     80      if (device.name == "Device 1") {
     81        throw new Error("Invoke error!");
     82      }
     83      Assert.equal(payload.encrypted, "encryptedpayload");
     84    }),
     85  };
     86  const fxai = FxaInternalMock();
     87  const sendTab = new SendTab(commands, fxai);
     88  sendTab._encrypt = (bytes, device) => {
     89    if (device.name == "Device 2") {
     90      throw new Error("Encrypt error!");
     91    }
     92    return "encryptedpayload";
     93  };
     94  const to = [
     95    { name: "Device 1" },
     96    { name: "Device 2" },
     97    { id: "dev3", name: "Device 3" },
     98  ];
     99  // although we are sending to 3 devices, only 1 is successful - so there's
    100  // only 1 streamID we care about. However, we've created IDs even for the
    101  // failing items - so it's "4"
    102  const expectedTelemetryStreamID = "4";
    103  const tab = { title: "Foo", url: "https://foo.bar/" };
    104  const report = await sendTab.send(to, tab);
    105  Assert.equal(report.succeeded.length, 1);
    106  Assert.equal(report.failed.length, 2);
    107  Assert.equal(report.succeeded[0].name, "Device 3");
    108  Assert.equal(report.failed[0].device.name, "Device 1");
    109  Assert.equal(report.failed[0].error.message, "Invoke error!");
    110  Assert.equal(report.failed[1].device.name, "Device 2");
    111  Assert.equal(report.failed[1].error.message, "Encrypt error!");
    112  Assert.ok(commands.invoke.calledTwice);
    113  Assert.deepEqual(fxai.telemetry._events, [
    114    {
    115      object: "command-sent",
    116      method: COMMAND_SENDTAB_TAIL,
    117      value: "dev3-san",
    118      extra: { flowID: "1", streamID: expectedTelemetryStreamID },
    119    },
    120  ]);
    121  const sendEvents = Glean.fxa.sendtabSent.testGetValue();
    122  Assert.equal(sendEvents.length, 1);
    123  Assert.deepEqual(sendEvents[0].extra, {
    124    flow_id: "1",
    125    hashed_device_id: "dev3-san",
    126    stream_id: expectedTelemetryStreamID,
    127  });
    128 });
    129 
    130 add_task(async function test_sendtab_send_rate_limit() {
    131  const rateLimitReject = {
    132    code: 429,
    133    retryAfter: 5,
    134    retryAfterLocalized: "retry after 5 seconds",
    135  };
    136  const fxAccounts = {
    137    fxAccountsClient: new MockFxAccountsClient(),
    138    getUserAccountData() {
    139      return {};
    140    },
    141    telemetry: new TelemetryMock(),
    142  };
    143  let rejected = false;
    144  let invoked = 0;
    145  fxAccounts.fxAccountsClient.invokeCommand = async function invokeCommand() {
    146    invoked++;
    147    Assert.lessOrEqual(invoked, 2, "only called twice and not more");
    148    if (rejected) {
    149      return {};
    150    }
    151    rejected = true;
    152    return Promise.reject(rateLimitReject);
    153  };
    154  const commands = new FxAccountsCommands(fxAccounts);
    155  const sendTab = new SendTab(commands, fxAccounts);
    156  sendTab._encrypt = () => "encryptedpayload";
    157 
    158  const tab = { title: "Foo", url: "https://foo.bar/" };
    159  let report = await sendTab.send([{ name: "Device 1" }], tab);
    160  Assert.equal(report.succeeded.length, 0);
    161  Assert.equal(report.failed.length, 1);
    162  Assert.equal(report.failed[0].error, rateLimitReject);
    163 
    164  report = await sendTab.send([{ name: "Device 1" }], tab);
    165  Assert.equal(report.succeeded.length, 0);
    166  Assert.equal(report.failed.length, 1);
    167  Assert.ok(
    168    report.failed[0].error.message.includes(
    169      "Invoke for " +
    170        "https://identity.mozilla.com/cmd/open-uri is rate-limited"
    171    )
    172  );
    173 
    174  commands._invokeRateLimitExpiry = Date.now() - 1000;
    175  report = await sendTab.send([{ name: "Device 1" }], tab);
    176  Assert.equal(report.succeeded.length, 1);
    177  Assert.equal(report.failed.length, 0);
    178 });
    179 
    180 add_task(async function test_sendtab_receive() {
    181  // We are testing 'receive' here, but might as well go through 'send'
    182  // to package the data and for additional testing...
    183  const commands = {
    184    _invokes: [],
    185    invoke(cmd, device, payload) {
    186      this._invokes.push({ cmd, device, payload });
    187    },
    188  };
    189 
    190  // Clear events from other test cases.
    191  Services.fog.testResetFOG();
    192 
    193  const fxai = FxaInternalMock();
    194  const sendTab = new SendTab(commands, fxai);
    195  sendTab._encrypt = bytes => {
    196    return bytes;
    197  };
    198  sendTab._decrypt = bytes => {
    199    return bytes;
    200  };
    201  const tab = { title: "tab title", url: "http://example.com" };
    202  const to = [{ id: "devid", name: "The Device" }];
    203  const reason = "push";
    204 
    205  await sendTab.send(to, tab);
    206  Assert.equal(commands._invokes.length, 1);
    207 
    208  for (let { cmd, device, payload } of commands._invokes) {
    209    Assert.equal(cmd, COMMAND_SENDTAB);
    210    // Older Firefoxes would send a plaintext flowID in the top-level payload.
    211    // Test that we sensibly ignore it.
    212    Assert.ok(!payload.hasOwnProperty("flowID"));
    213    // change it - ensure we still get what we expect in telemetry later.
    214    payload.flowID = "ignore-me";
    215    Assert.deepEqual(await sendTab.handle(device.id, payload, reason), {
    216      title: "tab title",
    217      uri: "http://example.com",
    218    });
    219  }
    220 
    221  Assert.deepEqual(fxai.telemetry._events, [
    222    {
    223      object: "command-sent",
    224      method: COMMAND_SENDTAB_TAIL,
    225      value: "devid-san",
    226      extra: { flowID: "1", streamID: "2" },
    227    },
    228    {
    229      object: "command-received",
    230      method: COMMAND_SENDTAB_TAIL,
    231      value: "devid-san",
    232      extra: { flowID: "1", streamID: "2", reason },
    233    },
    234  ]);
    235  const sendEvents = Glean.fxa.sendtabSent.testGetValue();
    236  Assert.equal(sendEvents.length, 1);
    237  Assert.deepEqual(sendEvents[0].extra, {
    238    flow_id: "1",
    239    hashed_device_id: "devid-san",
    240    stream_id: "2",
    241  });
    242  const recdEvents = Glean.fxa.sendtabReceived.testGetValue();
    243  Assert.equal(recdEvents.length, 1);
    244  Assert.deepEqual(recdEvents[0].extra, {
    245    flow_id: "1",
    246    hashed_device_id: "devid-san",
    247    reason,
    248    stream_id: "2",
    249  });
    250 });
    251 
    252 // Test that a client which only sends the flowID in the envelope and not in the
    253 // encrypted body gets recorded without the flowID.
    254 add_task(async function test_sendtab_receive_old_client() {
    255  // Clear events from other test cases.
    256  Services.fog.testResetFOG();
    257 
    258  const fxai = FxaInternalMock();
    259  const sendTab = new SendTab(null, fxai);
    260  sendTab._decrypt = bytes => {
    261    return bytes;
    262  };
    263  const data = { entries: [{ title: "title", url: "url" }] };
    264  // No 'flowID' in the encrypted payload, no 'streamID' anywhere.
    265  const payload = {
    266    flowID: "flow-id",
    267    encrypted: new TextEncoder().encode(JSON.stringify(data)),
    268  };
    269  const reason = "push";
    270  await sendTab.handle("sender-id", payload, reason);
    271  Assert.deepEqual(fxai.telemetry._events, [
    272    {
    273      object: "command-received",
    274      method: COMMAND_SENDTAB_TAIL,
    275      value: "sender-id-san",
    276      // deepEqual doesn't ignore undefined, but our telemetry code and
    277      // JSON.stringify() do...
    278      extra: { flowID: undefined, streamID: undefined, reason },
    279    },
    280  ]);
    281  const recdEvents = Glean.fxa.sendtabReceived.testGetValue();
    282  Assert.equal(recdEvents.length, 1);
    283  Assert.deepEqual(recdEvents[0].extra, {
    284    hashed_device_id: "sender-id-san",
    285    reason,
    286  });
    287 });
    288 
    289 add_task(function test_commands_getReason() {
    290  const fxAccounts = {
    291    async withCurrentAccountState(cb) {
    292      await cb({});
    293    },
    294  };
    295  const commands = new FxAccountsCommands(fxAccounts);
    296  const testCases = [
    297    {
    298      receivedIndex: 0,
    299      currentIndex: 0,
    300      expectedReason: "poll",
    301      message: "should return reason 'poll'",
    302    },
    303    {
    304      receivedIndex: 7,
    305      currentIndex: 3,
    306      expectedReason: "push-missed",
    307      message: "should return reason 'push-missed'",
    308    },
    309    {
    310      receivedIndex: 2,
    311      currentIndex: 8,
    312      expectedReason: "push",
    313      message: "should return reason 'push'",
    314    },
    315  ];
    316  for (const tc of testCases) {
    317    const reason = commands._getReason(tc.receivedIndex, tc.currentIndex);
    318    Assert.equal(reason, tc.expectedReason, tc.message);
    319  }
    320 });
    321 
    322 add_task(async function test_commands_pollDeviceCommands_push() {
    323  // Server state.
    324  const remoteMessages = [
    325    {
    326      index: 11,
    327      data: {},
    328    },
    329    {
    330      index: 12,
    331      data: {},
    332    },
    333  ];
    334  const remoteIndex = 12;
    335 
    336  // Local state.
    337  const pushIndexReceived = 11;
    338  const accountState = {
    339    data: {
    340      device: {
    341        lastCommandIndex: 10,
    342      },
    343    },
    344    getUserAccountData() {
    345      return this.data;
    346    },
    347    updateUserAccountData(data) {
    348      this.data = data;
    349    },
    350  };
    351 
    352  const fxAccounts = {
    353    async withCurrentAccountState(cb) {
    354      await cb(accountState);
    355    },
    356  };
    357  const commands = new FxAccountsCommands(fxAccounts);
    358  const mockCommands = sinon.mock(commands);
    359  mockCommands.expects("_fetchDeviceCommands").once().withArgs(11).returns({
    360    index: remoteIndex,
    361    messages: remoteMessages,
    362  });
    363  mockCommands
    364    .expects("_handleCommands")
    365    .once()
    366    .withArgs(remoteMessages, pushIndexReceived);
    367  await commands.pollDeviceCommands(pushIndexReceived);
    368 
    369  mockCommands.verify();
    370  Assert.equal(accountState.data.device.lastCommandIndex, 12);
    371 });
    372 
    373 add_task(
    374  async function test_commands_pollDeviceCommands_push_already_fetched() {
    375    // Local state.
    376    const pushIndexReceived = 12;
    377    const accountState = {
    378      data: {
    379        device: {
    380          lastCommandIndex: 12,
    381        },
    382      },
    383      getUserAccountData() {
    384        return this.data;
    385      },
    386      updateUserAccountData(data) {
    387        this.data = data;
    388      },
    389    };
    390 
    391    const fxAccounts = {
    392      async withCurrentAccountState(cb) {
    393        await cb(accountState);
    394      },
    395    };
    396    const commands = new FxAccountsCommands(fxAccounts);
    397    const mockCommands = sinon.mock(commands);
    398    mockCommands.expects("_fetchDeviceCommands").never();
    399    mockCommands.expects("_handleCommands").never();
    400    await commands.pollDeviceCommands(pushIndexReceived);
    401 
    402    mockCommands.verify();
    403    Assert.equal(accountState.data.device.lastCommandIndex, 12);
    404  }
    405 );
    406 
    407 add_task(async function test_commands_handleCommands() {
    408  // This test ensures that `_getReason` is being called by
    409  // `_handleCommands` with the expected parameters.
    410  const pushIndexReceived = 12;
    411  const senderID = "6d09f6c4-89b2-41b3-a0ac-e4c2502b5485";
    412  const remoteMessageIndex = 8;
    413  const remoteMessages = [
    414    {
    415      index: remoteMessageIndex,
    416      data: {
    417        command: COMMAND_SENDTAB,
    418        payload: {
    419          encrypted: {},
    420        },
    421        sender: senderID,
    422      },
    423    },
    424  ];
    425 
    426  const fxAccounts = {
    427    async withCurrentAccountState(cb) {
    428      await cb({});
    429    },
    430  };
    431  const commands = new FxAccountsCommands(fxAccounts);
    432  commands.sendTab.handle = () => {
    433    return {
    434      title: "testTitle",
    435      uri: "https://testURI",
    436    };
    437  };
    438  commands._fxai.device = {
    439    refreshDeviceList: () => {},
    440    recentDeviceList: [
    441      {
    442        id: senderID,
    443      },
    444    ],
    445  };
    446  const mockCommands = sinon.mock(commands);
    447  mockCommands
    448    .expects("_getReason")
    449    .once()
    450    .withExactArgs(pushIndexReceived, remoteMessageIndex);
    451  mockCommands.expects("_notifyFxATabsReceived").once();
    452  await commands._handleCommands(remoteMessages, pushIndexReceived);
    453  mockCommands.verify();
    454 });
    455 
    456 add_task(async function test_commands_handleCommands_invalid_tab() {
    457  // This test ensures that `_getReason` is being called by
    458  // `_handleCommands` with the expected parameters.
    459  const pushIndexReceived = 12;
    460  const senderID = "6d09f6c4-89b2-41b3-a0ac-e4c2502b5485";
    461  const remoteMessageIndex = 8;
    462  const remoteMessages = [
    463    {
    464      index: remoteMessageIndex,
    465      data: {
    466        command: COMMAND_SENDTAB,
    467        payload: {
    468          encrypted: {},
    469        },
    470        sender: senderID,
    471      },
    472    },
    473  ];
    474 
    475  const fxAccounts = {
    476    async withCurrentAccountState(cb) {
    477      await cb({});
    478    },
    479  };
    480  const commands = new FxAccountsCommands(fxAccounts);
    481  commands.sendTab.handle = () => {
    482    return {
    483      title: "badUriTab",
    484      uri: "file://path/to/pdf",
    485    };
    486  };
    487  commands._fxai.device = {
    488    refreshDeviceList: () => {},
    489    recentDeviceList: [
    490      {
    491        id: senderID,
    492      },
    493    ],
    494  };
    495  const mockCommands = sinon.mock(commands);
    496  mockCommands
    497    .expects("_getReason")
    498    .once()
    499    .withExactArgs(pushIndexReceived, remoteMessageIndex);
    500  // We shouldn't have tried to open a tab with an invalid uri
    501  mockCommands.expects("_notifyFxATabsReceived").never();
    502 
    503  await commands._handleCommands(remoteMessages, pushIndexReceived);
    504  mockCommands.verify();
    505 });
    506 
    507 add_task(
    508  async function test_commands_pollDeviceCommands_push_local_state_empty() {
    509    // Server state.
    510    const remoteMessages = [
    511      {
    512        index: 11,
    513        data: {},
    514      },
    515      {
    516        index: 12,
    517        data: {},
    518      },
    519    ];
    520    const remoteIndex = 12;
    521 
    522    // Local state.
    523    const pushIndexReceived = 11;
    524    const accountState = {
    525      data: {
    526        device: {},
    527      },
    528      getUserAccountData() {
    529        return this.data;
    530      },
    531      updateUserAccountData(data) {
    532        this.data = data;
    533      },
    534    };
    535 
    536    const fxAccounts = {
    537      async withCurrentAccountState(cb) {
    538        await cb(accountState);
    539      },
    540    };
    541    const commands = new FxAccountsCommands(fxAccounts);
    542    const mockCommands = sinon.mock(commands);
    543    mockCommands.expects("_fetchDeviceCommands").once().withArgs(11).returns({
    544      index: remoteIndex,
    545      messages: remoteMessages,
    546    });
    547    mockCommands
    548      .expects("_handleCommands")
    549      .once()
    550      .withArgs(remoteMessages, pushIndexReceived);
    551    await commands.pollDeviceCommands(pushIndexReceived);
    552 
    553    mockCommands.verify();
    554    Assert.equal(accountState.data.device.lastCommandIndex, 12);
    555  }
    556 );
    557 
    558 add_task(async function test_commands_pollDeviceCommands_scheduled_local() {
    559  // Server state.
    560  const remoteMessages = [
    561    {
    562      index: 11,
    563      data: {},
    564    },
    565    {
    566      index: 12,
    567      data: {},
    568    },
    569  ];
    570  const remoteIndex = 12;
    571  const pushIndexReceived = 0;
    572  // Local state.
    573  const accountState = {
    574    data: {
    575      device: {
    576        lastCommandIndex: 10,
    577      },
    578    },
    579    getUserAccountData() {
    580      return this.data;
    581    },
    582    updateUserAccountData(data) {
    583      this.data = data;
    584    },
    585  };
    586 
    587  const fxAccounts = {
    588    async withCurrentAccountState(cb) {
    589      await cb(accountState);
    590    },
    591  };
    592  const commands = new FxAccountsCommands(fxAccounts);
    593  const mockCommands = sinon.mock(commands);
    594  mockCommands.expects("_fetchDeviceCommands").once().withArgs(11).returns({
    595    index: remoteIndex,
    596    messages: remoteMessages,
    597  });
    598  mockCommands
    599    .expects("_handleCommands")
    600    .once()
    601    .withArgs(remoteMessages, pushIndexReceived);
    602  await commands.pollDeviceCommands();
    603 
    604  mockCommands.verify();
    605  Assert.equal(accountState.data.device.lastCommandIndex, 12);
    606 });
    607 
    608 add_task(
    609  async function test_commands_pollDeviceCommands_scheduled_local_state_empty() {
    610    // Server state.
    611    const remoteMessages = [
    612      {
    613        index: 11,
    614        data: {},
    615      },
    616      {
    617        index: 12,
    618        data: {},
    619      },
    620    ];
    621    const remoteIndex = 12;
    622    const pushIndexReceived = 0;
    623    // Local state.
    624    const accountState = {
    625      data: {
    626        device: {},
    627      },
    628      getUserAccountData() {
    629        return this.data;
    630      },
    631      updateUserAccountData(data) {
    632        this.data = data;
    633      },
    634    };
    635 
    636    const fxAccounts = {
    637      async withCurrentAccountState(cb) {
    638        await cb(accountState);
    639      },
    640    };
    641    const commands = new FxAccountsCommands(fxAccounts);
    642    const mockCommands = sinon.mock(commands);
    643    mockCommands.expects("_fetchDeviceCommands").once().withArgs(0).returns({
    644      index: remoteIndex,
    645      messages: remoteMessages,
    646    });
    647    mockCommands
    648      .expects("_handleCommands")
    649      .once()
    650      .withArgs(remoteMessages, pushIndexReceived);
    651    await commands.pollDeviceCommands();
    652 
    653    mockCommands.verify();
    654    Assert.equal(accountState.data.device.lastCommandIndex, 12);
    655  }
    656 );
    657 
    658 add_task(async function test_send_tab_keys_regenerated_if_lost() {
    659  const commands = {
    660    _invokes: [],
    661    invoke(cmd, device, payload) {
    662      this._invokes.push({ cmd, device, payload });
    663    },
    664  };
    665 
    666  // Local state.
    667  const accountState = {
    668    data: {
    669      // Since the device object has no
    670      // sendTabKeys, it will recover
    671      // when we attempt to get the
    672      // encryptedSendTabKeys
    673      device: {
    674        lastCommandIndex: 10,
    675      },
    676      encryptedSendTabKeys: "keys",
    677    },
    678    getUserAccountData() {
    679      return this.data;
    680    },
    681    updateUserAccountData(data) {
    682      this.data = data;
    683    },
    684  };
    685 
    686  const fxAccounts = {
    687    async withCurrentAccountState(cb) {
    688      await cb(accountState);
    689    },
    690    async getUserAccountData(data) {
    691      return accountState.getUserAccountData(data);
    692    },
    693    telemetry: new TelemetryMock(),
    694  };
    695  const sendTab = new SendTab(commands, fxAccounts);
    696  let generateEncryptedKeysCalled = false;
    697  sendTab._generateAndPersistEncryptedCommandKey = async () => {
    698    generateEncryptedKeysCalled = true;
    699  };
    700  await sendTab.getEncryptedCommandKeys();
    701  Assert.ok(generateEncryptedKeysCalled);
    702 });
    703 
    704 add_task(async function test_send_tab_keys_are_not_regenerated_if_not_lost() {
    705  const commands = {
    706    _invokes: [],
    707    invoke(cmd, device, payload) {
    708      this._invokes.push({ cmd, device, payload });
    709    },
    710  };
    711 
    712  // Local state.
    713  const accountState = {
    714    data: {
    715      // Since the device object has
    716      // sendTabKeys, it will not try
    717      // to regenerate them
    718      // when we attempt to get the
    719      // encryptedSendTabKeys
    720      device: {
    721        lastCommandIndex: 10,
    722        sendTabKeys: "keys",
    723      },
    724      encryptedSendTabKeys: "encrypted-keys",
    725    },
    726    getUserAccountData() {
    727      return this.data;
    728    },
    729    updateUserAccountData(data) {
    730      this.data = data;
    731    },
    732  };
    733 
    734  const fxAccounts = {
    735    async withCurrentAccountState(cb) {
    736      await cb(accountState);
    737    },
    738    async getUserAccountData(data) {
    739      return accountState.getUserAccountData(data);
    740    },
    741    telemetry: new TelemetryMock(),
    742  };
    743  const sendTab = new SendTab(commands, fxAccounts);
    744  let generateEncryptedKeysCalled = false;
    745  sendTab._generateAndPersistEncryptedCommandKey = async () => {
    746    generateEncryptedKeysCalled = true;
    747  };
    748  await sendTab.getEncryptedCommandKeys();
    749  Assert.ok(!generateEncryptedKeysCalled);
    750 });