tor-browser

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

head_nimbus_trainhop.js (9787B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 // NOTE: this head support file should be used along with the
      7 // one we are importing eslint globals from, and to be listed
      8 // in the xpcshell.toml `head` property value right after it for
      9 // the xpcshell test file that are meant to test Nimbus-driven
     10 // train-hopping.
     11 
     12 /* import-globals-from ../../../../extensions/newtab/test/xpcshell/head.js */
     13 
     14 /* exported assertNewTabResourceMapping, assertTrainhopAddonNimbusExposure,
     15 *          assertTrainhopAddonVersionPref,
     16 *          asyncAssertNewTabAddon, asyncAssertNimbusTrainhopAddonStaged,
     17 *          cancelPendingInstall, mockAboutNewTabUninit, server,
     18 *          BUILTIN_ADDON_ID, BUILTIN_ADDON_VERSION,
     19 *          BUILTIN_LOCATION_NAME, PROFILE_LOCATION_NAME,
     20 *          TRAINHOP_NIMBUS_FEATURE_ID,
     21 *          TRAINHOP_SCHEDULED_UPDATE_STATE_DELAY_PREF,
     22 *          TRAINHOP_SCHEDULED_UPDATE_STATE_TIMEOUT_PREF */
     23 
     24 const {
     25  AboutNewTabResourceMapping,
     26  BUILTIN_ADDON_ID,
     27  TRAINHOP_NIMBUS_FEATURE_ID,
     28  TRAINHOP_NIMBUS_FIRST_STARTUP_FEATURE_ID,
     29  TRAINHOP_XPI_BASE_URL_PREF,
     30  TRAINHOP_SCHEDULED_UPDATE_STATE_TIMEOUT_PREF,
     31  TRAINHOP_SCHEDULED_UPDATE_STATE_DELAY_PREF,
     32 } = ChromeUtils.importESModule(
     33  "resource:///modules/AboutNewTabResourceMapping.sys.mjs"
     34 );
     35 const { ExperimentAPI, NimbusFeatures } = ChromeUtils.importESModule(
     36  "resource://nimbus/ExperimentAPI.sys.mjs"
     37 );
     38 const { NimbusTestUtils } = ChromeUtils.importESModule(
     39  "resource://testing-common/NimbusTestUtils.sys.mjs"
     40 );
     41 
     42 const BUILTIN_LOCATION_NAME = "app-builtin-addons";
     43 const PROFILE_LOCATION_NAME = "app-profile";
     44 
     45 // Set from the setup task based on the currently installed
     46 // newtab built-in add-on version.
     47 let BUILTIN_ADDON_VERSION;
     48 
     49 NimbusTestUtils.init(this);
     50 
     51 const server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });
     52 Services.prefs.setStringPref(TRAINHOP_XPI_BASE_URL_PREF, "http://example.com/");
     53 
     54 // Defaults to "system" signedState for xpi with the newtab builtin add-id.
     55 AddonTestUtils.usePrivilegedSignatures = addonId =>
     56  addonId === BUILTIN_ADDON_ID ? "system" : false;
     57 
     58 add_setup(async function nimbusTestsSetup() {
     59  const { cleanup: nimbusTestCleanup } = await NimbusTestUtils.setupTest();
     60  registerCleanupFunction(nimbusTestCleanup);
     61 
     62  const builtinAddon = await asyncAssertNewTabAddon({
     63    locationName: "app-builtin-addons",
     64  });
     65  Assert.ok(builtinAddon?.version, "Got a builtin add-on version");
     66  BUILTIN_ADDON_VERSION = builtinAddon.version;
     67 
     68  // When we stage installs and then cancel them, `XPIInstall` won't be able to
     69  // remove the staging directory (which is expected to be empty) until the
     70  // next restart. This causes an `AddonTestUtils` assertion to fail because we
     71  // don't expect any staging directory at the end of the tests. That's why we
     72  // remove this directory in the cleanup function defined below.
     73  //
     74  // We only remove the staging directory and that will only works if the
     75  // directory is empty, otherwise an unchaught error will be thrown (on
     76  // purpose).
     77  registerCleanupFunction(() => {
     78    const profileDir = do_get_profile();
     79    const stagingDir = profileDir.clone();
     80    stagingDir.append("extensions");
     81    stagingDir.append("staged");
     82    stagingDir.exists() && stagingDir.remove(/* recursive */ false);
     83  });
     84 });
     85 
     86 /**
     87 * Setting up a simulated train-hop add-on version for testing.
     88 *
     89 * @param {object} params
     90 * @param {string} [params.updateAddonVersion]
     91 *   The version of the train-hop add-on to set in the simulated feature enrolling.
     92 * @returns {Promise<object>} A promise that resolves with an object containing:
     93 *   - fakeNimbusVariables: The fake Nimbus feature variables used in the simulated Nimbus feature enrolling.
     94 *   - nimbusFeatureCleanup: The cleanup function for the Nimbus feature.
     95 */
     96 async function setupNimbusTrainhopAddon({ updateAddonVersion }) {
     97  info(`Setting up simulated train-hop add-on version ${updateAddonVersion}`);
     98  let fakeNewTabXPI = AddonTestUtils.createTempWebExtensionFile({
     99    manifest: {
    100      version: updateAddonVersion,
    101      browser_specific_settings: {
    102        gecko: { id: BUILTIN_ADDON_ID },
    103      },
    104    },
    105    files: {
    106      "lib/NewTabGleanUtils.sys.mjs": `
    107        export const NewTabGleanUtils = {
    108          registerMetricsAndPings() {},
    109        };
    110      `,
    111    },
    112  });
    113 
    114  const xpi_download_path = "data/newtab.xpi";
    115 
    116  server.registerFile(`/${xpi_download_path}`, fakeNewTabXPI, () => {
    117    info(`Server got request for ${xpi_download_path}`);
    118  });
    119 
    120  const fakeNimbusVariables = {
    121    xpi_download_path,
    122    addon_version: updateAddonVersion,
    123  };
    124 
    125  await ExperimentAPI.ready();
    126  const nimbusFeatureCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
    127    {
    128      featureId: TRAINHOP_NIMBUS_FEATURE_ID,
    129      value: fakeNimbusVariables,
    130    },
    131    { isRollout: true }
    132  );
    133  // Sanity checks.
    134  Assert.deepEqual(
    135    NimbusFeatures[TRAINHOP_NIMBUS_FEATURE_ID].getAllVariables(),
    136    fakeNimbusVariables,
    137    "Got the expected variables from the nimbus feature"
    138  );
    139 
    140  return { fakeNimbusVariables, nimbusFeatureCleanup };
    141 }
    142 
    143 /**
    144 * Verifies that a newtab add-on exists in the expected location and optionally asserts
    145 * that the add-on has the expected version.
    146 *
    147 * @param {object} params
    148 * @param {string} params.locationName
    149 *   XPIProvider location name where the newtab add-on is expected to be installed into.
    150 * @param {string} [params.version]
    151 *   The expected newtab add-on version (only verified if set).
    152 * @returns {Promise<AddonWrapper>}
    153 *   A promise that resolves to the AddonWrapper instance for the newtab add-on.
    154 */
    155 async function asyncAssertNewTabAddon({ locationName, version }) {
    156  const newtabAddon = await AddonManager.getAddonByID(BUILTIN_ADDON_ID);
    157  Assert.equal(
    158    newtabAddon?.locationName,
    159    locationName,
    160    "Got the expected newtab add-on locationName"
    161  );
    162  if (version) {
    163    Assert.equal(
    164      newtabAddon?.version,
    165      version,
    166      "Got the expected newtab add-on version"
    167    );
    168  }
    169  return newtabAddon;
    170 }
    171 
    172 /**
    173 * Verifies that the expected staged installation for the train-hop add-on exists.
    174 *
    175 * @param {object} params
    176 * @param {string} params.updateAddonVersion
    177 *   The add-on version expected to be installed by the simulated feature enrolling.
    178 * @returns {Promise<AddonInstall>}
    179 *   A promise that resolves with to the AddonInstall instance for the train-hop
    180 *   add-on version expected to be found staged.
    181 */
    182 async function asyncAssertNimbusTrainhopAddonStaged({ updateAddonVersion }) {
    183  const pendingInstall = (await AddonManager.getAllInstalls()).find(
    184    install => install.addon.id === BUILTIN_ADDON_ID
    185  );
    186  Assert.equal(
    187    pendingInstall?.state,
    188    AddonManager.STATE_POSTPONED,
    189    "Expect a pending install for the newtab add-on to be found"
    190  );
    191 
    192  Assert.deepEqual(
    193    {
    194      existingVersion: pendingInstall.existingAddon.version,
    195      existingLocationName: pendingInstall.existingAddon.locationName,
    196      updateVersion: pendingInstall.addon.version,
    197      updateLocationName: pendingInstall.addon.locationName,
    198    },
    199    {
    200      existingVersion: BUILTIN_ADDON_VERSION,
    201      existingLocationName: BUILTIN_LOCATION_NAME,
    202      updateVersion: updateAddonVersion,
    203      updateLocationName: PROFILE_LOCATION_NAME,
    204    },
    205    "Got the expected version and locationName pendingInstall existing and updated add-on"
    206  );
    207  return { pendingInstall };
    208 }
    209 
    210 /**
    211 * Asserts that an exposure event for train-hop Nimbus feature has been recorded or
    212 * not recorded yet.
    213 *
    214 * @param {object}  params
    215 * @param {boolean} params.expectedExposure
    216 */
    217 function assertTrainhopAddonNimbusExposure({ expectedExposure }) {
    218  const enrollmentMetadata =
    219    NimbusFeatures[TRAINHOP_NIMBUS_FEATURE_ID].getEnrollmentMetadata();
    220  Assert.deepEqual(
    221    Glean.nimbusEvents.exposure
    222      .testGetValue("events")
    223      ?.map(ev => ev.extra)
    224      .filter(ev => ev.feature_id == TRAINHOP_NIMBUS_FEATURE_ID) ?? [],
    225    expectedExposure
    226      ? [
    227          {
    228            feature_id: TRAINHOP_NIMBUS_FEATURE_ID,
    229            branch: enrollmentMetadata.branch,
    230            experiment: enrollmentMetadata.slug,
    231          },
    232        ]
    233      : [],
    234    expectedExposure
    235      ? "Got the expected exposure Glean event for the newtabTrainhopAddon Nimbus feature"
    236      : "Got no exposure Glean event for the newtabTrainhopAddon as expected"
    237  );
    238 }
    239 
    240 function assertTrainhopAddonVersionPref(expectedTrainhopAddonVersion) {
    241  Assert.equal(
    242    Services.prefs.getStringPref(
    243      "browser.newtabpage.trainhopAddon.version",
    244      ""
    245    ),
    246    expectedTrainhopAddonVersion,
    247    expectedTrainhopAddonVersion
    248      ? "Expect browser.newtab.trainhopAddon.version about:config pref to be set while client is enrolled"
    249      : "Expect browser.newtab.trainhopAddon.version about:config pref to be empty while client is unenrolled"
    250  );
    251 }
    252 
    253 /**
    254 * Cancels a pending add-on installation and awaits the cancellation to be completed.
    255 *
    256 * @param {AddonInstall} pendingInstall
    257 *   Instance of the pending AddonInstall to cancel.
    258 * @returns {Promise<void>}
    259 *   A promise that resolves when the AddonInstall instance has been successfully cancelled.
    260 */
    261 async function cancelPendingInstall(pendingInstall) {
    262  const cancelDeferred = Promise.withResolvers();
    263  pendingInstall.addListener({
    264    onInstallCancelled() {
    265      cancelDeferred.resolve();
    266    },
    267  });
    268  pendingInstall.cancel();
    269  await cancelDeferred.promise;
    270 }
    271 
    272 // Mock browser restart scenario.
    273 function mockAboutNewTabUninit() {
    274  AboutNewTab.uninit();
    275  AboutNewTabResourceMapping.initialized = false;
    276  AboutNewTabResourceMapping._rootURISpec = null;
    277  AboutNewTabResourceMapping._addonVersion = null;
    278  AboutNewTabResourceMapping._addonListener = null;
    279 }