tor-browser

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

test_web_channel.js (37214B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 const { ON_PROFILE_CHANGE_NOTIFICATION, WEBCHANNEL_ID, log } =
      7  ChromeUtils.importESModule("resource://gre/modules/FxAccountsCommon.sys.mjs");
      8 const { CryptoUtils } = ChromeUtils.importESModule(
      9  "moz-src:///services/crypto/modules/utils.sys.mjs"
     10 );
     11 const { FxAccountsWebChannel, FxAccountsWebChannelHelpers } =
     12  ChromeUtils.importESModule(
     13    "resource://gre/modules/FxAccountsWebChannel.sys.mjs"
     14  );
     15 
     16 const { PREF_LAST_FXA_USER_EMAIL, PREF_LAST_FXA_USER_UID } =
     17  ChromeUtils.importESModule("resource://gre/modules/FxAccountsCommon.sys.mjs");
     18 
     19 const URL_STRING = "https://example.com";
     20 
     21 const mockSendingContext = {
     22  browsingContext: { top: { embedderElement: {} } },
     23  principal: {},
     24  eventTarget: {},
     25 };
     26 
     27 add_setup(function setup() {
     28  // The profile service requires the directory service to have been initialized.
     29  Cc["@mozilla.org/xre/directory-provider;1"].getService(Ci.nsIXREDirProvider);
     30 });
     31 
     32 add_test(function () {
     33  validationHelper(undefined, "Error: Missing configuration options");
     34 
     35  validationHelper(
     36    {
     37      channel_id: WEBCHANNEL_ID,
     38    },
     39    "Error: Missing 'content_uri' option"
     40  );
     41 
     42  validationHelper(
     43    {
     44      content_uri: "bad uri",
     45      channel_id: WEBCHANNEL_ID,
     46    },
     47    /NS_ERROR_MALFORMED_URI/
     48  );
     49 
     50  validationHelper(
     51    {
     52      content_uri: URL_STRING,
     53    },
     54    "Error: Missing 'channel_id' option"
     55  );
     56 
     57  run_next_test();
     58 });
     59 
     60 add_task(async function test_rejection_reporting() {
     61  Services.prefs.setBoolPref(
     62    "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess",
     63    false
     64  );
     65 
     66  let mockMessage = {
     67    command: "fxaccounts:login",
     68    messageId: "1234",
     69    data: { email: "testuser@testuser.com", uid: "testuser" },
     70  };
     71 
     72  let channel = new FxAccountsWebChannel({
     73    channel_id: WEBCHANNEL_ID,
     74    content_uri: URL_STRING,
     75    helpers: {
     76      login(accountData) {
     77        equal(
     78          accountData.email,
     79          "testuser@testuser.com",
     80          "Should forward incoming message data to the helper"
     81        );
     82        return Promise.reject(new Error("oops"));
     83      },
     84    },
     85  });
     86 
     87  let promiseSend = new Promise(resolve => {
     88    channel._channel.send = (message, context) => {
     89      resolve({ message, context });
     90    };
     91  });
     92 
     93  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
     94 
     95  let { message, context } = await promiseSend;
     96 
     97  equal(context, mockSendingContext, "Should forward the original context");
     98  equal(
     99    message.command,
    100    "fxaccounts:login",
    101    "Should include the incoming command"
    102  );
    103  equal(message.messageId, "1234", "Should include the message ID");
    104  equal(
    105    message.data.error.message,
    106    "Error: oops",
    107    "Should convert the error message to a string"
    108  );
    109  notStrictEqual(
    110    message.data.error.stack,
    111    null,
    112    "Should include the stack for JS error rejections"
    113  );
    114 });
    115 
    116 add_test(function test_exception_reporting() {
    117  let mockMessage = {
    118    command: "fxaccounts:sync_preferences",
    119    messageId: "5678",
    120    data: { entryPoint: "fxa:verification_complete" },
    121  };
    122 
    123  let channel = new FxAccountsWebChannel({
    124    channel_id: WEBCHANNEL_ID,
    125    content_uri: URL_STRING,
    126    helpers: {
    127      openSyncPreferences(browser, entryPoint) {
    128        equal(
    129          entryPoint,
    130          "fxa:verification_complete",
    131          "Should forward incoming message data to the helper"
    132        );
    133        throw new TypeError("splines not reticulated");
    134      },
    135    },
    136  });
    137 
    138  channel._channel.send = (message, context) => {
    139    equal(context, mockSendingContext, "Should forward the original context");
    140    equal(
    141      message.command,
    142      "fxaccounts:sync_preferences",
    143      "Should include the incoming command"
    144    );
    145    equal(message.messageId, "5678", "Should include the message ID");
    146    equal(
    147      message.data.error.message,
    148      "TypeError: splines not reticulated",
    149      "Should convert the exception to a string"
    150    );
    151    notStrictEqual(
    152      message.data.error.stack,
    153      null,
    154      "Should include the stack for JS exceptions"
    155    );
    156 
    157    run_next_test();
    158  };
    159 
    160  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
    161 });
    162 
    163 add_test(function test_error_message_remove_profile_path() {
    164  const errors = {
    165    windows: {
    166      err: new Error(
    167        "Win error 183 during operation rename on file C:\\Users\\Some Computer\\AppData\\Roaming\\" +
    168          "Mozilla\\Firefox\\Profiles\\dbzjmzxa.default\\signedInUser.json (Cannot create a file)"
    169      ),
    170      expected:
    171        "Error: Win error 183 during operation rename on file C:[REDACTED]signedInUser.json (Cannot create a file)",
    172    },
    173    unix: {
    174      err: new Error(
    175        "Unix error 28 during operation write on file /Users/someuser/Library/Application Support/" +
    176          "Firefox/Profiles/dbzjmzxa.default-release-7/signedInUser.json (No space left on device)"
    177      ),
    178      expected:
    179        "Error: Unix error 28 during operation write on file [REDACTED]signedInUser.json (No space left on device)",
    180    },
    181    netpath: {
    182      err: new Error(
    183        "Win error 32 during operation rename on file \\\\SVC.LOC\\HOMEDIRS$\\USERNAME\\Mozilla\\" +
    184          "Firefox\\Profiles\\dbzjmzxa.default-release-7\\signedInUser.json (No space left on device)"
    185      ),
    186      expected:
    187        "Error: Win error 32 during operation rename on file [REDACTED]signedInUser.json (No space left on device)",
    188    },
    189    mount: {
    190      err: new Error(
    191        "Win error 649 during operation rename on file C:\\SnapVolumes\\MountPoints\\" +
    192          "{9e399ec5-0000-0000-0000-100000000000}\\SVROOT\\Users\\username\\AppData\\Roaming\\Mozilla\\Firefox\\" +
    193          "Profiles\\dbzjmzxa.default-release\\signedInUser.json (The create operation failed)"
    194      ),
    195      expected:
    196        "Error: Win error 649 during operation rename on file C:[REDACTED]signedInUser.json " +
    197        "(The create operation failed)",
    198    },
    199  };
    200  const mockMessage = {
    201    command: "fxaccounts:sync_preferences",
    202    messageId: "1234",
    203  };
    204  const channel = new FxAccountsWebChannel({
    205    channel_id: WEBCHANNEL_ID,
    206    content_uri: URL_STRING,
    207  });
    208 
    209  let testNum = 0;
    210  const toTest = Object.keys(errors).length;
    211  for (const key in errors) {
    212    let error = errors[key];
    213    channel._channel.send = message => {
    214      equal(
    215        message.data.error.message,
    216        error.expected,
    217        "Should remove the profile path from the error message"
    218      );
    219      testNum++;
    220      if (testNum === toTest) {
    221        run_next_test();
    222      }
    223    };
    224    channel._sendError(error.err, mockMessage, mockSendingContext);
    225  }
    226 });
    227 
    228 add_test(function test_profile_image_change_message() {
    229  var mockMessage = {
    230    command: "profile:change",
    231    data: { uid: "foo" },
    232  };
    233 
    234  makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) {
    235    Assert.equal(data, "foo");
    236    run_next_test();
    237  });
    238 
    239  var channel = new FxAccountsWebChannel({
    240    channel_id: WEBCHANNEL_ID,
    241    content_uri: URL_STRING,
    242  });
    243 
    244  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
    245 });
    246 
    247 add_test(function test_login_message() {
    248  let mockMessage = {
    249    command: "fxaccounts:login",
    250    data: { email: "testuser@testuser.com" },
    251  };
    252 
    253  let channel = new FxAccountsWebChannel({
    254    channel_id: WEBCHANNEL_ID,
    255    content_uri: URL_STRING,
    256    helpers: {
    257      login(accountData) {
    258        Assert.equal(accountData.email, "testuser@testuser.com");
    259        run_next_test();
    260        return Promise.resolve();
    261      },
    262    },
    263  });
    264 
    265  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
    266 });
    267 
    268 add_test(function test_oauth_login() {
    269  const mockData = {
    270    code: "oauth code",
    271    state: "state parameter",
    272    declinedSyncEngines: ["tabs", "creditcards"],
    273    offeredSyncEngines: ["tabs", "creditcards", "history"],
    274  };
    275  const mockMessage = {
    276    command: "fxaccounts:oauth_login",
    277    data: mockData,
    278  };
    279  const channel = new FxAccountsWebChannel({
    280    channel_id: WEBCHANNEL_ID,
    281    content_uri: URL_STRING,
    282    helpers: {
    283      oauthLogin(data) {
    284        Assert.deepEqual(data, mockData);
    285        run_next_test();
    286        return Promise.resolve();
    287      },
    288    },
    289  });
    290  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
    291 });
    292 
    293 add_test(function test_logout_message() {
    294  let mockMessage = {
    295    command: "fxaccounts:logout",
    296    data: { uid: "foo" },
    297  };
    298 
    299  let channel = new FxAccountsWebChannel({
    300    channel_id: WEBCHANNEL_ID,
    301    content_uri: URL_STRING,
    302    helpers: {
    303      logout(uid) {
    304        Assert.equal(uid, "foo");
    305        run_next_test();
    306        return Promise.resolve();
    307      },
    308    },
    309  });
    310 
    311  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
    312 });
    313 
    314 add_test(function test_delete_message() {
    315  let mockMessage = {
    316    command: "fxaccounts:delete",
    317    data: { uid: "foo" },
    318  };
    319 
    320  let channel = new FxAccountsWebChannel({
    321    channel_id: WEBCHANNEL_ID,
    322    content_uri: URL_STRING,
    323    helpers: {
    324      logout(uid) {
    325        Assert.equal(uid, "foo");
    326        run_next_test();
    327        return Promise.resolve();
    328      },
    329    },
    330  });
    331 
    332  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
    333 });
    334 
    335 add_test(function test_can_link_account_message() {
    336  let mockMessage = {
    337    command: "fxaccounts:can_link_account",
    338    data: { email: "testuser@testuser.com", uid: "testuser" },
    339  };
    340 
    341  let channel = new FxAccountsWebChannel({
    342    channel_id: WEBCHANNEL_ID,
    343    content_uri: URL_STRING,
    344    helpers: {
    345      _selectableProfilesEnabled() {
    346        return false;
    347      },
    348      shouldAllowRelink(acctData) {
    349        Assert.deepEqual(acctData, mockMessage.data);
    350        run_next_test();
    351      },
    352    },
    353  });
    354 
    355  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
    356 });
    357 
    358 add_test(function test_sync_preferences_message() {
    359  let mockMessage = {
    360    command: "fxaccounts:sync_preferences",
    361    data: { entryPoint: "fxa:verification_complete" },
    362  };
    363 
    364  let channel = new FxAccountsWebChannel({
    365    channel_id: WEBCHANNEL_ID,
    366    content_uri: URL_STRING,
    367    helpers: {
    368      openSyncPreferences(browser, entryPoint) {
    369        Assert.equal(entryPoint, "fxa:verification_complete");
    370        Assert.equal(
    371          browser,
    372          mockSendingContext.browsingContext.top.embedderElement
    373        );
    374        run_next_test();
    375      },
    376    },
    377  });
    378 
    379  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
    380 });
    381 
    382 add_test(function test_fxa_status_message() {
    383  let mockMessage = {
    384    command: "fxaccounts:fxa_status",
    385    messageId: 123,
    386    data: {
    387      service: "sync",
    388      context: "fx_desktop_v3",
    389    },
    390  };
    391 
    392  let channel = new FxAccountsWebChannel({
    393    channel_id: WEBCHANNEL_ID,
    394    content_uri: URL_STRING,
    395    helpers: {
    396      async getFxaStatus(service, sendingContext, isPairing, context) {
    397        Assert.equal(service, "sync");
    398        Assert.equal(sendingContext, mockSendingContext);
    399        Assert.ok(!isPairing);
    400        Assert.equal(context, "fx_desktop_v3");
    401        return {
    402          signedInUser: {
    403            email: "testuser@testuser.com",
    404            sessionToken: "session-token",
    405            uid: "uid",
    406            verified: true,
    407          },
    408          capabilities: {
    409            engines: ["creditcards", "addresses"],
    410          },
    411        };
    412      },
    413    },
    414  });
    415 
    416  channel._channel = {
    417    send(response) {
    418      Assert.equal(response.command, "fxaccounts:fxa_status");
    419      Assert.equal(response.messageId, 123);
    420 
    421      let signedInUser = response.data.signedInUser;
    422      Assert.ok(!!signedInUser);
    423      Assert.equal(signedInUser.email, "testuser@testuser.com");
    424      Assert.equal(signedInUser.sessionToken, "session-token");
    425      Assert.equal(signedInUser.uid, "uid");
    426      Assert.equal(signedInUser.verified, true);
    427 
    428      deepEqual(response.data.capabilities.engines, [
    429        "creditcards",
    430        "addresses",
    431      ]);
    432 
    433      run_next_test();
    434    },
    435  };
    436 
    437  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
    438 });
    439 
    440 add_test(function test_respond_to_invalid_commands() {
    441  let mockMessageLogout = {
    442    command: "fxaccounts:lagaut", // intentional typo.
    443    messageId: 123,
    444    data: {},
    445  };
    446 
    447  let channel = new FxAccountsWebChannel({
    448    channel_id: WEBCHANNEL_ID,
    449    content_uri: URL_STRING,
    450  });
    451  channel._channel = {
    452    send(response) {
    453      Assert.equal("fxaccounts:lagaut", response.command);
    454      Assert.ok(!!response.data);
    455      Assert.ok(!!response.data.error);
    456 
    457      run_next_test();
    458    },
    459  };
    460 
    461  channel._channelCallback(
    462    WEBCHANNEL_ID,
    463    mockMessageLogout,
    464    mockSendingContext
    465  );
    466 });
    467 
    468 add_test(function test_unrecognized_message() {
    469  let mockMessage = {
    470    command: "fxaccounts:unrecognized",
    471    data: {},
    472  };
    473 
    474  let channel = new FxAccountsWebChannel({
    475    channel_id: WEBCHANNEL_ID,
    476    content_uri: URL_STRING,
    477  });
    478 
    479  // no error is expected.
    480  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
    481  run_next_test();
    482 });
    483 
    484 add_test(function test_helpers_should_allow_relink_same_account() {
    485  let helpers = new FxAccountsWebChannelHelpers();
    486 
    487  helpers.setPreviousAccountHashPref("testuser");
    488  Assert.ok(
    489    helpers.shouldAllowRelink({
    490      email: "testuser@testuser.com",
    491      uid: "testuser",
    492    })
    493  );
    494 
    495  run_next_test();
    496 });
    497 
    498 add_test(function test_helpers_should_allow_relink_different_email() {
    499  let helpers = new FxAccountsWebChannelHelpers();
    500 
    501  helpers.setPreviousAccountHashPref("testuser");
    502 
    503  helpers._promptForRelink = acctName => {
    504    return acctName === "allowed_to_relink@testuser.com";
    505  };
    506 
    507  Assert.ok(
    508    helpers.shouldAllowRelink({
    509      uid: "uid",
    510      email: "allowed_to_relink@testuser.com",
    511    })
    512  );
    513  Assert.ok(
    514    !helpers.shouldAllowRelink({
    515      uid: "uid",
    516      email: "not_allowed_to_relink@testuser.com",
    517    })
    518  );
    519 
    520  run_next_test();
    521 });
    522 
    523 add_task(async function test_helpers_login_without_customize_sync() {
    524  let helpers = new FxAccountsWebChannelHelpers({
    525    fxAccounts: {
    526      getSignedInUser() {
    527        return Promise.resolve(null);
    528      },
    529      _internal: {
    530        setSignedInUser(accountData) {
    531          return new Promise(resolve => {
    532            // ensure fxAccounts is informed of the new user being signed in.
    533            Assert.equal(accountData.email, "testuser@testuser.com");
    534 
    535            // verifiedCanLinkAccount should be stripped in the data.
    536            Assert.equal(false, "verifiedCanLinkAccount" in accountData);
    537 
    538            resolve();
    539          });
    540        },
    541      },
    542      telemetry: {
    543        recordConnection: sinon.spy(),
    544      },
    545    },
    546    weaveXPCOM: {
    547      whenLoaded() {},
    548      Weave: {
    549        Service: {
    550          configure() {},
    551        },
    552      },
    553    },
    554  });
    555 
    556  // ensure the previous account pref is overwritten.
    557  helpers.setPreviousAccountHashPref("lastuser");
    558 
    559  await helpers.login({
    560    uid: "testuser",
    561    email: "testuser@testuser.com",
    562    verifiedCanLinkAccount: true,
    563    customizeSync: false,
    564  });
    565  Assert.ok(
    566    helpers._fxAccounts.telemetry.recordConnection.calledWith([], "webchannel")
    567  );
    568 });
    569 
    570 add_task(async function test_helpers_login_set_previous_account_hash() {
    571  let helpers = new FxAccountsWebChannelHelpers({
    572    fxAccounts: {
    573      getSignedInUser() {
    574        return Promise.resolve(null);
    575      },
    576      _internal: {
    577        setSignedInUser() {
    578          return new Promise(resolve => {
    579            // previously signed in user preference is updated.
    580            Assert.equal(
    581              Services.prefs.getStringPref(PREF_LAST_FXA_USER_UID),
    582              CryptoUtils.sha256Base64("new_uid")
    583            );
    584            Assert.equal(
    585              Services.prefs.getStringPref(PREF_LAST_FXA_USER_EMAIL, ""),
    586              ""
    587            );
    588            resolve();
    589          });
    590        },
    591      },
    592      telemetry: {
    593        recordConnection() {},
    594      },
    595    },
    596    weaveXPCOM: {
    597      whenLoaded() {},
    598      Weave: {
    599        Service: {
    600          configure() {},
    601        },
    602      },
    603    },
    604  });
    605 
    606  // ensure the previous account pref is overwritten.
    607  helpers.setPreviousAccountHashPref("last_uid");
    608 
    609  await helpers.login({
    610    uid: "new_uid",
    611    email: "newuser@testuser.com",
    612    verifiedCanLinkAccount: true,
    613    customizeSync: false,
    614    verified: true,
    615  });
    616 });
    617 
    618 add_task(async function test_helpers_login_another_user_signed_in() {
    619  let helpers = new FxAccountsWebChannelHelpers({
    620    fxAccounts: {
    621      getSignedInUser() {
    622        return Promise.resolve({ uid: "foo" });
    623      },
    624      _internal: {
    625        setSignedInUser(accountData) {
    626          return new Promise(resolve => {
    627            // ensure fxAccounts is informed of the new user being signed in.
    628            Assert.equal(accountData.email, "testuser@testuser.com");
    629            resolve();
    630          });
    631        },
    632      },
    633      telemetry: {
    634        recordConnection: sinon.spy(),
    635      },
    636    },
    637    weaveXPCOM: {
    638      whenLoaded() {},
    639      Weave: {
    640        Service: {
    641          configure() {},
    642        },
    643      },
    644    },
    645  });
    646  helpers._disconnect = sinon.spy();
    647 
    648  await helpers.login({
    649    uid: "testuser",
    650    email: "testuser@testuser.com",
    651    verifiedCanLinkAccount: true,
    652    customizeSync: false,
    653  });
    654  Assert.ok(
    655    helpers._fxAccounts.telemetry.recordConnection.calledWith([], "webchannel")
    656  );
    657  Assert.ok(helpers._disconnect.called);
    658 });
    659 
    660 // The FxA server sends the `login` command after the user is signed in
    661 // when upgrading from third-party auth to password + sync
    662 add_task(async function test_helpers_login_same_user_signed_in() {
    663  let updateUserAccountDataCalled = false;
    664  let setSignedInUserCalled = false;
    665 
    666  let helpers = new FxAccountsWebChannelHelpers({
    667    fxAccounts: {
    668      getSignedInUser() {
    669        return Promise.resolve({
    670          uid: "testuser",
    671          email: "testuser@testuser.com",
    672        });
    673      },
    674      _internal: {
    675        updateUserAccountData(accountData) {
    676          updateUserAccountDataCalled = true;
    677          Assert.equal(accountData.email, "testuser@testuser.com");
    678          Assert.equal(accountData.uid, "testuser");
    679          return Promise.resolve();
    680        },
    681        setSignedInUser() {
    682          setSignedInUserCalled = true;
    683          return Promise.resolve();
    684        },
    685      },
    686      telemetry: {
    687        recordConnection: sinon.spy(),
    688      },
    689    },
    690    weaveXPCOM: {
    691      whenLoaded() {},
    692      Weave: {
    693        Service: {
    694          configure() {},
    695        },
    696      },
    697    },
    698  });
    699  helpers._disconnect = sinon.spy();
    700 
    701  await helpers.login({
    702    uid: "testuser",
    703    email: "testuser@testuser.com",
    704    verifiedCanLinkAccount: true,
    705    customizeSync: false,
    706  });
    707 
    708  Assert.ok(
    709    updateUserAccountDataCalled,
    710    "updateUserAccountData should be called"
    711  );
    712  Assert.ok(!setSignedInUserCalled, "setSignedInUser should not be called");
    713  Assert.ok(!helpers._disconnect.called, "_disconnect should not be called");
    714  Assert.ok(
    715    helpers._fxAccounts.telemetry.recordConnection.calledWith([], "webchannel")
    716  );
    717 });
    718 
    719 add_task(async function test_helpers_login_with_customize_sync() {
    720  let helpers = new FxAccountsWebChannelHelpers({
    721    fxAccounts: {
    722      _internal: {
    723        setSignedInUser(accountData) {
    724          return new Promise(resolve => {
    725            // ensure fxAccounts is informed of the new user being signed in.
    726            Assert.equal(accountData.email, "testuser@testuser.com");
    727 
    728            // customizeSync should be stripped in the data.
    729            Assert.equal(false, "customizeSync" in accountData);
    730 
    731            resolve();
    732          });
    733        },
    734      },
    735      getSignedInUser() {
    736        return Promise.resolve(null);
    737      },
    738      telemetry: {
    739        recordConnection: sinon.spy(),
    740      },
    741    },
    742    weaveXPCOM: {
    743      whenLoaded() {},
    744      Weave: {
    745        Service: {
    746          configure() {},
    747        },
    748      },
    749    },
    750  });
    751 
    752  await helpers.login({
    753    uid: "testuser",
    754    email: "testuser@testuser.com",
    755    verifiedCanLinkAccount: true,
    756    customizeSync: true,
    757  });
    758  Assert.ok(
    759    helpers._fxAccounts.telemetry.recordConnection.calledWith([], "webchannel")
    760  );
    761 });
    762 
    763 add_task(async function test_helpers_persist_requested_services() {
    764  let accountData = null;
    765  const helpers = new FxAccountsWebChannelHelpers({
    766    fxAccounts: {
    767      _internal: {
    768        async setSignedInUser(newAccountData) {
    769          accountData = newAccountData;
    770          return accountData;
    771        },
    772        async updateUserAccountData(updatedFields) {
    773          accountData = { ...accountData, ...updatedFields };
    774          return accountData;
    775        },
    776      },
    777      async getSignedInUser() {
    778        return accountData;
    779      },
    780      telemetry: {
    781        recordConnection() {},
    782      },
    783    },
    784    weaveXPCOM: {
    785      whenLoaded() {},
    786      Weave: {
    787        Service: {},
    788      },
    789    },
    790  });
    791 
    792  await helpers.login({
    793    uid: "auid",
    794    email: "testuser@testuser.com",
    795    verifiedCanLinkAccount: true,
    796    services: {
    797      first_only: { x: 10 }, // this data is not in the update below.
    798      sync: { important: true },
    799    },
    800  });
    801 
    802  Assert.deepEqual(JSON.parse(accountData.requestedServices), {
    803    first_only: { x: 10 },
    804    sync: { important: true },
    805  });
    806  // A second "login" message without the services.
    807  await helpers.login({
    808    uid: "auid",
    809    email: "testuser@testuser.com",
    810    verifiedCanLinkAccount: true,
    811    services: {
    812      // the service is mentioned, but data is empty, so it's the old version of the data we want.
    813      sync: {},
    814      // a new service we never saw before, but we still want it.
    815      new: { name: "opted in" }, // not in original, but we want in the final.
    816    },
    817  });
    818  // the version with the data should remain.
    819  Assert.deepEqual(JSON.parse(accountData.requestedServices), {
    820    first_only: { x: 10 },
    821    sync: { important: true },
    822    new: { name: "opted in" },
    823  });
    824 });
    825 
    826 add_task(async function test_helpers_oauth_login_defers_sync_without_keys() {
    827  const accountState = {
    828    uid: "uid123",
    829    sessionToken: "session-token",
    830    email: "user@example.com",
    831    requestedServices: "",
    832  };
    833  const destroyOAuthToken = sinon.stub().resolves();
    834  const completeOAuthFlow = sinon
    835    .stub()
    836    .resolves({ scopedKeys: null, refreshToken: "refresh-token" });
    837  const setScopedKeys = sinon.spy();
    838  const setUserVerified = sinon.spy();
    839  const updateUserAccountData = sinon.stub().resolves();
    840 
    841  const helpers = new FxAccountsWebChannelHelpers({
    842    fxAccounts: {
    843      _internal: {
    844        async getUserAccountData() {
    845          return accountState;
    846        },
    847        completeOAuthFlow,
    848        destroyOAuthToken,
    849        setScopedKeys,
    850        updateUserAccountData,
    851        setUserVerified,
    852      },
    853    },
    854  });
    855 
    856  await helpers.oauthLogin({ code: "code", state: "state" });
    857 
    858  Assert.ok(setScopedKeys.notCalled);
    859  Assert.ok(updateUserAccountData.calledOnce);
    860  Assert.deepEqual(
    861    JSON.parse(updateUserAccountData.firstCall.args[0].requestedServices),
    862    null
    863  );
    864 });
    865 
    866 add_test(function test_helpers_open_sync_preferences() {
    867  let helpers = new FxAccountsWebChannelHelpers({
    868    fxAccounts: {},
    869  });
    870 
    871  let mockBrowser = {
    872    loadURI(uri) {
    873      Assert.equal(
    874        uri.spec,
    875        "about:preferences?entrypoint=fxa%3Averification_complete#sync"
    876      );
    877      run_next_test();
    878    },
    879  };
    880 
    881  helpers.openSyncPreferences(mockBrowser, "fxa:verification_complete");
    882 });
    883 
    884 add_task(async function test_helpers_getFxAStatus_engines_oauth() {
    885  let helpers = new FxAccountsWebChannelHelpers({
    886    fxAccounts: {
    887      _internal: {
    888        getUserAccountData() {
    889          return Promise.resolve({
    890            email: "testuser@testuser.com",
    891            sessionToken: "sessionToken",
    892            uid: "uid",
    893            verified: true,
    894          });
    895        },
    896      },
    897    },
    898    privateBrowsingUtils: {
    899      isBrowserPrivate: () => true,
    900    },
    901  });
    902 
    903  // disable the "addresses" engine.
    904  Services.prefs.setBoolPref("services.sync.engine.addresses.available", false);
    905  let fxaStatus = await helpers.getFxaStatus("sync", mockSendingContext);
    906  ok(!!fxaStatus);
    907  ok(!!fxaStatus.signedInUser);
    908  // in the oauth flows we expect all engines.
    909  deepEqual(fxaStatus.capabilities.engines.toSorted(), [
    910    "addons",
    911    "bookmarks",
    912    "creditcards",
    913    "history",
    914    "passwords",
    915    "prefs",
    916    "tabs",
    917  ]);
    918 
    919  // try again with addresses enabled.
    920  Services.prefs.setBoolPref("services.sync.engine.addresses.available", true);
    921  fxaStatus = await helpers.getFxaStatus("sync", mockSendingContext);
    922  deepEqual(fxaStatus.capabilities.engines.toSorted(), [
    923    "addons",
    924    "addresses",
    925    "bookmarks",
    926    "creditcards",
    927    "history",
    928    "passwords",
    929    "prefs",
    930    "tabs",
    931  ]);
    932 });
    933 
    934 add_task(async function test_helpers_getFxaStatus_allowed_signedInUser() {
    935  let wasCalled = {
    936    getUserAccountData: false,
    937    shouldAllowFxaStatus: false,
    938  };
    939 
    940  let helpers = new FxAccountsWebChannelHelpers({
    941    fxAccounts: {
    942      _internal: {
    943        getUserAccountData() {
    944          wasCalled.getUserAccountData = true;
    945          return Promise.resolve({
    946            email: "testuser@testuser.com",
    947            sessionToken: "sessionToken",
    948            uid: "uid",
    949            verified: true,
    950          });
    951        },
    952      },
    953    },
    954  });
    955 
    956  helpers.shouldAllowFxaStatus = (service, sendingContext) => {
    957    wasCalled.shouldAllowFxaStatus = true;
    958    Assert.equal(service, "sync");
    959    Assert.equal(sendingContext, mockSendingContext);
    960 
    961    return true;
    962  };
    963 
    964  return helpers.getFxaStatus("sync", mockSendingContext).then(fxaStatus => {
    965    Assert.ok(!!fxaStatus);
    966    Assert.ok(wasCalled.getUserAccountData);
    967    Assert.ok(wasCalled.shouldAllowFxaStatus);
    968 
    969    Assert.ok(!!fxaStatus.signedInUser);
    970    let { signedInUser } = fxaStatus;
    971 
    972    Assert.equal(signedInUser.email, "testuser@testuser.com");
    973    Assert.equal(signedInUser.sessionToken, "sessionToken");
    974    Assert.equal(signedInUser.uid, "uid");
    975    Assert.ok(signedInUser.verified);
    976 
    977    // These properties are filtered and should not
    978    // be returned to the requester.
    979    Assert.equal(false, "scopedKeys" in signedInUser);
    980  });
    981 });
    982 
    983 add_task(async function test_helpers_getFxaStatus_allowed_no_signedInUser() {
    984  let wasCalled = {
    985    getUserAccountData: false,
    986    shouldAllowFxaStatus: false,
    987  };
    988 
    989  let helpers = new FxAccountsWebChannelHelpers({
    990    fxAccounts: {
    991      _internal: {
    992        getUserAccountData() {
    993          wasCalled.getUserAccountData = true;
    994          return Promise.resolve(null);
    995        },
    996      },
    997    },
    998  });
    999 
   1000  helpers.shouldAllowFxaStatus = (service, sendingContext) => {
   1001    wasCalled.shouldAllowFxaStatus = true;
   1002    Assert.equal(service, "sync");
   1003    Assert.equal(sendingContext, mockSendingContext);
   1004 
   1005    return true;
   1006  };
   1007 
   1008  return helpers.getFxaStatus("sync", mockSendingContext).then(fxaStatus => {
   1009    Assert.ok(!!fxaStatus);
   1010    Assert.ok(wasCalled.getUserAccountData);
   1011    Assert.ok(wasCalled.shouldAllowFxaStatus);
   1012 
   1013    Assert.equal(null, fxaStatus.signedInUser);
   1014  });
   1015 });
   1016 
   1017 add_task(async function test_helpers_getFxaStatus_not_allowed() {
   1018  let wasCalled = {
   1019    getUserAccountData: false,
   1020    shouldAllowFxaStatus: false,
   1021  };
   1022 
   1023  let helpers = new FxAccountsWebChannelHelpers({
   1024    fxAccounts: {
   1025      _internal: {
   1026        getUserAccountData() {
   1027          wasCalled.getUserAccountData = true;
   1028          return Promise.resolve(null);
   1029        },
   1030      },
   1031    },
   1032  });
   1033 
   1034  helpers.shouldAllowFxaStatus = (
   1035    service,
   1036    sendingContext,
   1037    isPairing,
   1038    context
   1039  ) => {
   1040    wasCalled.shouldAllowFxaStatus = true;
   1041    Assert.equal(service, "sync");
   1042    Assert.equal(sendingContext, mockSendingContext);
   1043    Assert.ok(!isPairing);
   1044    Assert.equal(context, "fx_desktop_v3");
   1045 
   1046    return false;
   1047  };
   1048 
   1049  return helpers
   1050    .getFxaStatus("sync", mockSendingContext, false, "fx_desktop_v3")
   1051    .then(fxaStatus => {
   1052      Assert.ok(!!fxaStatus);
   1053      Assert.ok(!wasCalled.getUserAccountData);
   1054      Assert.ok(wasCalled.shouldAllowFxaStatus);
   1055 
   1056      Assert.equal(null, fxaStatus.signedInUser);
   1057    });
   1058 });
   1059 
   1060 add_task(
   1061  async function test_helpers_shouldAllowFxaStatus_sync_service_not_private_browsing() {
   1062    let wasCalled = {
   1063      isPrivateBrowsingMode: false,
   1064    };
   1065    let helpers = new FxAccountsWebChannelHelpers({});
   1066 
   1067    helpers.isPrivateBrowsingMode = sendingContext => {
   1068      wasCalled.isPrivateBrowsingMode = true;
   1069      Assert.equal(sendingContext, mockSendingContext);
   1070      return false;
   1071    };
   1072 
   1073    let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus(
   1074      "sync",
   1075      mockSendingContext,
   1076      false
   1077    );
   1078    Assert.ok(shouldAllowFxaStatus);
   1079    Assert.ok(wasCalled.isPrivateBrowsingMode);
   1080  }
   1081 );
   1082 
   1083 add_task(
   1084  async function test_helpers_shouldAllowFxaStatus_desktop_context_not_private_browsing() {
   1085    let wasCalled = {
   1086      isPrivateBrowsingMode: false,
   1087    };
   1088    let helpers = new FxAccountsWebChannelHelpers({});
   1089 
   1090    helpers.isPrivateBrowsingMode = sendingContext => {
   1091      wasCalled.isPrivateBrowsingMode = true;
   1092      Assert.equal(sendingContext, mockSendingContext);
   1093      return false;
   1094    };
   1095 
   1096    let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus(
   1097      "",
   1098      mockSendingContext,
   1099      false,
   1100      "fx_desktop_v3"
   1101    );
   1102    Assert.ok(shouldAllowFxaStatus);
   1103    Assert.ok(wasCalled.isPrivateBrowsingMode);
   1104  }
   1105 );
   1106 
   1107 add_task(
   1108  async function test_helpers_shouldAllowFxaStatus_oauth_service_not_private_browsing() {
   1109    let wasCalled = {
   1110      isPrivateBrowsingMode: false,
   1111    };
   1112    let helpers = new FxAccountsWebChannelHelpers({});
   1113 
   1114    helpers.isPrivateBrowsingMode = sendingContext => {
   1115      wasCalled.isPrivateBrowsingMode = true;
   1116      Assert.equal(sendingContext, mockSendingContext);
   1117      return false;
   1118    };
   1119 
   1120    let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus(
   1121      "dcdb5ae7add825d2",
   1122      mockSendingContext,
   1123      false
   1124    );
   1125    Assert.ok(shouldAllowFxaStatus);
   1126    Assert.ok(wasCalled.isPrivateBrowsingMode);
   1127  }
   1128 );
   1129 
   1130 add_task(
   1131  async function test_helpers_shouldAllowFxaStatus_no_service_not_private_browsing() {
   1132    let wasCalled = {
   1133      isPrivateBrowsingMode: false,
   1134    };
   1135    let helpers = new FxAccountsWebChannelHelpers({});
   1136 
   1137    helpers.isPrivateBrowsingMode = sendingContext => {
   1138      wasCalled.isPrivateBrowsingMode = true;
   1139      Assert.equal(sendingContext, mockSendingContext);
   1140      return false;
   1141    };
   1142 
   1143    let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus(
   1144      "",
   1145      mockSendingContext,
   1146      false
   1147    );
   1148    Assert.ok(shouldAllowFxaStatus);
   1149    Assert.ok(wasCalled.isPrivateBrowsingMode);
   1150  }
   1151 );
   1152 
   1153 add_task(
   1154  async function test_helpers_shouldAllowFxaStatus_sync_service_private_browsing() {
   1155    let wasCalled = {
   1156      isPrivateBrowsingMode: false,
   1157    };
   1158    let helpers = new FxAccountsWebChannelHelpers({});
   1159 
   1160    helpers.isPrivateBrowsingMode = sendingContext => {
   1161      wasCalled.isPrivateBrowsingMode = true;
   1162      Assert.equal(sendingContext, mockSendingContext);
   1163      return true;
   1164    };
   1165 
   1166    let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus(
   1167      "sync",
   1168      mockSendingContext,
   1169      false
   1170    );
   1171    Assert.ok(shouldAllowFxaStatus);
   1172    Assert.ok(wasCalled.isPrivateBrowsingMode);
   1173  }
   1174 );
   1175 
   1176 add_task(
   1177  async function test_helpers_shouldAllowFxaStatus_oauth_service_private_browsing() {
   1178    let wasCalled = {
   1179      isPrivateBrowsingMode: false,
   1180    };
   1181    let helpers = new FxAccountsWebChannelHelpers({});
   1182 
   1183    helpers.isPrivateBrowsingMode = sendingContext => {
   1184      wasCalled.isPrivateBrowsingMode = true;
   1185      Assert.equal(sendingContext, mockSendingContext);
   1186      return true;
   1187    };
   1188 
   1189    let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus(
   1190      "dcdb5ae7add825d2",
   1191      mockSendingContext,
   1192      false
   1193    );
   1194    Assert.ok(!shouldAllowFxaStatus);
   1195    Assert.ok(wasCalled.isPrivateBrowsingMode);
   1196  }
   1197 );
   1198 
   1199 add_task(
   1200  async function test_helpers_shouldAllowFxaStatus_oauth_service_pairing_private_browsing() {
   1201    let wasCalled = {
   1202      isPrivateBrowsingMode: false,
   1203    };
   1204    let helpers = new FxAccountsWebChannelHelpers({});
   1205 
   1206    helpers.isPrivateBrowsingMode = sendingContext => {
   1207      wasCalled.isPrivateBrowsingMode = true;
   1208      Assert.equal(sendingContext, mockSendingContext);
   1209      return true;
   1210    };
   1211 
   1212    let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus(
   1213      "dcdb5ae7add825d2",
   1214      mockSendingContext,
   1215      true
   1216    );
   1217    Assert.ok(shouldAllowFxaStatus);
   1218    Assert.ok(wasCalled.isPrivateBrowsingMode);
   1219  }
   1220 );
   1221 
   1222 add_task(
   1223  async function test_helpers_shouldAllowFxaStatus_no_service_private_browsing() {
   1224    let wasCalled = {
   1225      isPrivateBrowsingMode: false,
   1226    };
   1227    let helpers = new FxAccountsWebChannelHelpers({});
   1228 
   1229    helpers.isPrivateBrowsingMode = sendingContext => {
   1230      wasCalled.isPrivateBrowsingMode = true;
   1231      Assert.equal(sendingContext, mockSendingContext);
   1232      return true;
   1233    };
   1234 
   1235    let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus(
   1236      "",
   1237      mockSendingContext,
   1238      false
   1239    );
   1240    Assert.ok(!shouldAllowFxaStatus);
   1241    Assert.ok(wasCalled.isPrivateBrowsingMode);
   1242  }
   1243 );
   1244 
   1245 add_task(async function test_helpers_isPrivateBrowsingMode_private_browsing() {
   1246  let wasCalled = {
   1247    isBrowserPrivate: false,
   1248  };
   1249  let helpers = new FxAccountsWebChannelHelpers({
   1250    privateBrowsingUtils: {
   1251      isBrowserPrivate(browser) {
   1252        wasCalled.isBrowserPrivate = true;
   1253        Assert.equal(
   1254          browser,
   1255          mockSendingContext.browsingContext.top.embedderElement
   1256        );
   1257        return true;
   1258      },
   1259    },
   1260  });
   1261 
   1262  let isPrivateBrowsingMode = helpers.isPrivateBrowsingMode(mockSendingContext);
   1263  Assert.ok(isPrivateBrowsingMode);
   1264  Assert.ok(wasCalled.isBrowserPrivate);
   1265 });
   1266 
   1267 add_task(async function test_helpers_isPrivateBrowsingMode_private_browsing() {
   1268  let wasCalled = {
   1269    isBrowserPrivate: false,
   1270  };
   1271  let helpers = new FxAccountsWebChannelHelpers({
   1272    privateBrowsingUtils: {
   1273      isBrowserPrivate(browser) {
   1274        wasCalled.isBrowserPrivate = true;
   1275        Assert.equal(
   1276          browser,
   1277          mockSendingContext.browsingContext.top.embedderElement
   1278        );
   1279        return false;
   1280      },
   1281    },
   1282  });
   1283 
   1284  let isPrivateBrowsingMode = helpers.isPrivateBrowsingMode(mockSendingContext);
   1285  Assert.ok(!isPrivateBrowsingMode);
   1286  Assert.ok(wasCalled.isBrowserPrivate);
   1287 });
   1288 
   1289 add_task(async function test_helpers_change_password() {
   1290  let wasCalled = {
   1291    updateUserAccountData: false,
   1292    updateDeviceRegistration: false,
   1293  };
   1294  let helpers = new FxAccountsWebChannelHelpers({
   1295    fxAccounts: {
   1296      _internal: {
   1297        updateUserAccountData(credentials) {
   1298          return new Promise(resolve => {
   1299            Assert.ok(credentials.hasOwnProperty("email"));
   1300            Assert.ok(credentials.hasOwnProperty("uid"));
   1301            Assert.ok(credentials.hasOwnProperty("unwrapBKey"));
   1302            Assert.ok(credentials.hasOwnProperty("device"));
   1303            Assert.equal(null, credentials.device);
   1304            Assert.equal(null, credentials.encryptedSendTabKeys);
   1305            // "foo" isn't a field known by storage, so should be dropped.
   1306            Assert.ok(!credentials.hasOwnProperty("foo"));
   1307            wasCalled.updateUserAccountData = true;
   1308 
   1309            resolve();
   1310          });
   1311        },
   1312 
   1313        updateDeviceRegistration() {
   1314          Assert.equal(arguments.length, 0);
   1315          wasCalled.updateDeviceRegistration = true;
   1316          return Promise.resolve();
   1317        },
   1318      },
   1319    },
   1320  });
   1321  await helpers.changePassword({
   1322    email: "email",
   1323    uid: "uid",
   1324    unwrapBKey: "unwrapBKey",
   1325    foo: "foo",
   1326  });
   1327  Assert.ok(wasCalled.updateUserAccountData);
   1328  Assert.ok(wasCalled.updateDeviceRegistration);
   1329 });
   1330 
   1331 add_task(async function test_helpers_change_password_with_error() {
   1332  let wasCalled = {
   1333    updateUserAccountData: false,
   1334    updateDeviceRegistration: false,
   1335  };
   1336  let helpers = new FxAccountsWebChannelHelpers({
   1337    fxAccounts: {
   1338      _internal: {
   1339        updateUserAccountData() {
   1340          wasCalled.updateUserAccountData = true;
   1341          return Promise.reject();
   1342        },
   1343 
   1344        updateDeviceRegistration() {
   1345          wasCalled.updateDeviceRegistration = true;
   1346          return Promise.resolve();
   1347        },
   1348      },
   1349    },
   1350  });
   1351  try {
   1352    await helpers.changePassword({});
   1353    Assert.equal(false, "changePassword should have rejected");
   1354  } catch (_) {
   1355    Assert.ok(wasCalled.updateUserAccountData);
   1356    Assert.ok(!wasCalled.updateDeviceRegistration);
   1357  }
   1358 });
   1359 
   1360 function makeObserver(aObserveTopic, aObserveFunc) {
   1361  let callback = function (aSubject, aTopic, aData) {
   1362    log.debug("observed " + aTopic + " " + aData);
   1363    if (aTopic == aObserveTopic) {
   1364      removeMe();
   1365      aObserveFunc(aSubject, aTopic, aData);
   1366    }
   1367  };
   1368 
   1369  function removeMe() {
   1370    log.debug("removing observer for " + aObserveTopic);
   1371    Services.obs.removeObserver(callback, aObserveTopic);
   1372  }
   1373 
   1374  Services.obs.addObserver(callback, aObserveTopic);
   1375  return removeMe;
   1376 }
   1377 
   1378 function validationHelper(params, expected) {
   1379  try {
   1380    new FxAccountsWebChannel(params);
   1381  } catch (e) {
   1382    if (typeof expected === "string") {
   1383      return Assert.equal(e.toString(), expected);
   1384    }
   1385    return Assert.ok(e.toString().match(expected));
   1386  }
   1387  throw new Error("Validation helper error");
   1388 }