tor-browser

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

test_nimbus_newtabTrainhopAddon.js (17932B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 https://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 /* import-globals-from ../../../../extensions/newtab/test/xpcshell/head.js */
      7 
      8 /* import-globals-from head_nimbus_trainhop.js */
      9 
     10 const { AboutHomeStartupCache } = ChromeUtils.importESModule(
     11  "resource:///modules/AboutHomeStartupCache.sys.mjs"
     12 );
     13 const { sinon } = ChromeUtils.importESModule(
     14  "resource://testing-common/Sinon.sys.mjs"
     15 );
     16 
     17 add_task(async function test_download_and_staged_install_trainhop_addon() {
     18  Services.fog.testResetFOG();
     19 
     20  // Sanity check (verifies built-in add-on resources have been mapped).
     21  assertNewTabResourceMapping();
     22  await asyncAssertNewTabAddon({
     23    locationName: BUILTIN_LOCATION_NAME,
     24  });
     25  assertTrainhopAddonNimbusExposure({ expectedExposure: false });
     26 
     27  const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.123`;
     28 
     29  const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({
     30    updateAddonVersion,
     31  });
     32  assertTrainhopAddonVersionPref(updateAddonVersion);
     33 
     34  await AboutNewTabResourceMapping.updateTrainhopAddonState();
     35  const { pendingInstall } = await asyncAssertNimbusTrainhopAddonStaged({
     36    updateAddonVersion,
     37  });
     38 
     39  // Verify that we are still using the New Tab resources from the builtin add-on.
     40  assertNewTabResourceMapping();
     41  // Verify that no exposure event has been recorded until the New Tab resources
     42  // for the train-hop add-on version are actually in use.
     43  assertTrainhopAddonNimbusExposure({ expectedExposure: false });
     44 
     45  await cancelPendingInstall(pendingInstall);
     46  await nimbusFeatureCleanup();
     47  assertTrainhopAddonVersionPref("");
     48 });
     49 
     50 add_task(async function test_trainhop_addon_download_errors() {
     51  server.registerPathHandler("/data/invalid-zip.xpi", (_request, response) => {
     52    response.write("NOT_A_VALID_XPI");
     53  });
     54 
     55  const brokenManifestXPI = await AddonTestUtils.createTempXPIFile({
     56    "manifest.json": "not valid JSON",
     57  });
     58  server.registerPathHandler(
     59    "/data/broken-manifest.xpi",
     60    (request, response) => {
     61      server._handler._writeFileResponse(request, brokenManifestXPI, response);
     62    }
     63  );
     64 
     65  const invalidManifestXPI = AddonTestUtils.createTempWebExtensionFile({
     66    manifest: {
     67      version: `${BUILTIN_ADDON_VERSION}.123`,
     68      browser_specific_settings: {
     69        gecko: { id: BUILTIN_ADDON_ID },
     70      },
     71      // Invalid manifest property that is expected to hit a manifest
     72      // validation error.
     73      background: { scripts: "it-should-be-an-array.js" },
     74    },
     75  });
     76  server.registerPathHandler(
     77    "/data/invalid-manifest.xpi",
     78    (request, response) => {
     79      server._handler._writeFileResponse(request, invalidManifestXPI, response);
     80    }
     81  );
     82 
     83  const invalidSignatureXPI = AddonTestUtils.createTempWebExtensionFile({
     84    manifest: {
     85      version: `${BUILTIN_ADDON_VERSION}.123`,
     86      browser_specific_settings: {
     87        gecko: { id: BUILTIN_ADDON_ID },
     88      },
     89    },
     90  });
     91  server.registerPathHandler(
     92    "/data/invalid-signature.xpi",
     93    (request, response) => {
     94      server._handler._writeFileResponse(
     95        request,
     96        invalidSignatureXPI,
     97        response
     98      );
     99    }
    100  );
    101 
    102  await ExperimentAPI.ready();
    103  await testDownloadError("data/non-existing.xpi");
    104  await testDownloadError("data/invalid-zip.xpi");
    105  await testDownloadError("data/broken-manifest.xpi");
    106  await testDownloadError(
    107    "data/invalid-manifest.xpi",
    108    `${BUILTIN_ADDON_VERSION}.123`
    109  );
    110  const oldUsePrivilegedSignatures = AddonTestUtils.usePrivilegedSignatures;
    111  AddonTestUtils.usePrivilegedSignatures = false;
    112  await testDownloadError(
    113    "data/invalid-signature.xpi",
    114    `${BUILTIN_ADDON_VERSION}.123`,
    115    AddonManager.STATE_CANCELLED
    116  );
    117  AddonTestUtils.usePrivilegedSignatures = oldUsePrivilegedSignatures;
    118 
    119  async function testDownloadError(
    120    xpi_download_path,
    121    addon_version = "9999.0",
    122    expectedInstallState = AddonManager.STATE_DOWNLOAD_FAILED
    123  ) {
    124    Services.fog.testResetFOG();
    125    const nimbusFeatureCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
    126      {
    127        featureId: TRAINHOP_NIMBUS_FEATURE_ID,
    128        value: {
    129          xpi_download_path,
    130          addon_version,
    131        },
    132      },
    133      { isRollout: true }
    134    );
    135 
    136    const promiseDownloadFailed =
    137      AddonTestUtils.promiseInstallEvent("onDownloadFailed");
    138    const promiseDownloadEnded =
    139      AddonTestUtils.promiseInstallEvent("onDownloadEnded");
    140 
    141    info("Trigger download and install train-hop add-on version");
    142    const promiseTrainhopRequest =
    143      AboutNewTabResourceMapping.updateTrainhopAddonState();
    144 
    145    info("Wait for AddonManager onDownloadFailed");
    146    const [install] = await Promise.race([
    147      promiseDownloadFailed,
    148      // Ensure the test fails right away if the unexpected
    149      // onDownloadEnded install event is resolved.
    150      promiseDownloadEnded,
    151    ]);
    152 
    153    Assert.equal(
    154      install.state,
    155      expectedInstallState,
    156      `Expect install state to be ${AddonManager._states.get(expectedInstallState)}`
    157    );
    158 
    159    info("Wait for updateTrainhopAddonState call to be resolved as expected");
    160    await promiseTrainhopRequest;
    161 
    162    Assert.deepEqual(
    163      await AddonManager.getAllInstalls(),
    164      [],
    165      "Expect no pending install to be found"
    166    );
    167 
    168    assertTrainhopAddonNimbusExposure({ expectedExposure: false });
    169    await nimbusFeatureCleanup();
    170  }
    171 });
    172 
    173 add_task(async function test_trainhop_cancel_on_version_check() {
    174  await testTrainhopCancelOnVersionCheck({
    175    updateAddonVersion: BUILTIN_ADDON_VERSION,
    176    message:
    177      "Test train-hop add-on version equal to the built-in add-on version",
    178  });
    179  await testTrainhopCancelOnVersionCheck({
    180    updateAddonVersion: "140.0.1",
    181    message:
    182      "Test train-hop add-on version lower than the built-in add-on version",
    183  });
    184 
    185  async function testTrainhopCancelOnVersionCheck({
    186    updateAddonVersion,
    187    message,
    188  }) {
    189    Services.fog.testResetFOG();
    190    info(message);
    191    // Sanity check (verifies built-in add-on resources have been mapped).
    192    assertNewTabResourceMapping();
    193    assertTrainhopAddonNimbusExposure({ expectedExposure: false });
    194 
    195    await asyncAssertNewTabAddon({
    196      locationName: "app-builtin-addons",
    197    });
    198    const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({
    199      updateAddonVersion,
    200    });
    201 
    202    await AboutNewTabResourceMapping.updateTrainhopAddonState();
    203    Assert.deepEqual(
    204      await AddonManager.getAllInstalls(),
    205      [],
    206      "Expect no pending install to be found"
    207    );
    208 
    209    info("Verify the built-in version is still the one installed");
    210    await asyncAssertNewTabAddon({
    211      locationName: "app-builtin-addons",
    212      version: BUILTIN_ADDON_VERSION,
    213    });
    214    // Verify that we are still using the New Tab resources from the builtin add-on.
    215    assertNewTabResourceMapping();
    216    assertTrainhopAddonNimbusExposure({ expectedExposure: false });
    217 
    218    await nimbusFeatureCleanup();
    219  }
    220 });
    221 
    222 add_task(async function test_trainhop_addon_after_browser_restart() {
    223  // Sanity check (verifies built-in add-on resources have been mapped).
    224  assertNewTabResourceMapping();
    225  await asyncAssertNewTabAddon({
    226    locationName: BUILTIN_LOCATION_NAME,
    227  });
    228  assertTrainhopAddonVersionPref("");
    229 
    230  const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.123`;
    231 
    232  const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({
    233    updateAddonVersion,
    234  });
    235  assertTrainhopAddonVersionPref(updateAddonVersion);
    236 
    237  await AboutNewTabResourceMapping.updateTrainhopAddonState();
    238  await asyncAssertNimbusTrainhopAddonStaged({
    239    updateAddonVersion,
    240  });
    241  // Verify that we are still using the New Tab resources from the builtin add-on.
    242  assertNewTabResourceMapping();
    243  Assert.ok(
    244    !Glean.newtab.addonXpiUsed.testGetValue(),
    245    "Probe says we're not using an XPI"
    246  );
    247 
    248  info(
    249    "Simulated browser restart while train-hop add-on is pending installation"
    250  );
    251  Services.fog.testResetFOG();
    252  mockAboutNewTabUninit();
    253  await AddonTestUtils.promiseRestartManager();
    254  AboutNewTab.init();
    255 
    256  await asyncAssertNewTabAddon({
    257    locationName: PROFILE_LOCATION_NAME,
    258    version: updateAddonVersion,
    259  });
    260  const trainhopAddonPolicy = WebExtensionPolicy.getByID(BUILTIN_ADDON_ID);
    261  Assert.equal(
    262    trainhopAddonPolicy?.extension?.version,
    263    updateAddonVersion,
    264    "Got newtab WebExtensionPolicy instance for the train-hop add-on version"
    265  );
    266 
    267  assertNewTabResourceMapping(trainhopAddonPolicy.extension.rootURI.spec);
    268  Assert.ok(
    269    Glean.newtab.addonXpiUsed.testGetValue(),
    270    "Probe says we're using an XPI"
    271  );
    272 
    273  Assert.deepEqual(
    274    await AddonManager.getAllInstalls(),
    275    [],
    276    "Expect no pending install to be found"
    277  );
    278 
    279  await AboutNewTabResourceMapping.updateTrainhopAddonState();
    280  Assert.deepEqual(
    281    await AddonManager.getAllInstalls(),
    282    [],
    283    "Expect no additional pending install for the same train-hop add-on version"
    284  );
    285 
    286  assertTrainhopAddonNimbusExposure({ expectedExposure: true });
    287  assertTrainhopAddonVersionPref(updateAddonVersion);
    288 
    289  info("Simulate newtabTrainhopAddon nimbus feature unenrolled");
    290  await nimbusFeatureCleanup();
    291  assertTrainhopAddonVersionPref("");
    292 
    293  // Expect train-hop add-on to not be uninstalled yet because it is still
    294  // used by newtab resources mapping.
    295  await AboutNewTabResourceMapping.updateTrainhopAddonState();
    296  assertNewTabResourceMapping(trainhopAddonPolicy.extension.rootURI.spec);
    297  await asyncAssertNewTabAddon({
    298    locationName: PROFILE_LOCATION_NAME,
    299    version: updateAddonVersion,
    300  });
    301 
    302  info(
    303    "Simulated browser restart while newtabTrainhopAddon nimbus feature is unenrolled"
    304  );
    305  mockAboutNewTabUninit();
    306  await AddonTestUtils.promiseRestartManager();
    307  AboutNewTab.init();
    308 
    309  // Expected bundled newtab resources mapping for this session.
    310  assertNewTabResourceMapping();
    311  await AboutNewTabResourceMapping.updateTrainhopAddonState();
    312  await asyncAssertNewTabAddon({
    313    locationName: BUILTIN_LOCATION_NAME,
    314    version: BUILTIN_ADDON_VERSION,
    315  });
    316 });
    317 
    318 add_task(async function test_builtin_version_upgrades() {
    319  // Sanity check (verifies built-in addon resources have been mapped).
    320  assertNewTabResourceMapping();
    321  await asyncAssertNewTabAddon({
    322    locationName: BUILTIN_LOCATION_NAME,
    323    version: BUILTIN_ADDON_VERSION,
    324  });
    325  assertTrainhopAddonVersionPref("");
    326 
    327  const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.123`;
    328 
    329  const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({
    330    updateAddonVersion,
    331  });
    332  assertTrainhopAddonVersionPref(updateAddonVersion);
    333 
    334  await AboutNewTabResourceMapping.updateTrainhopAddonState();
    335  await asyncAssertNimbusTrainhopAddonStaged({
    336    updateAddonVersion,
    337  });
    338  // Verify that we are still using the New Tab resources from the builtin add-on.
    339  assertNewTabResourceMapping();
    340 
    341  info(
    342    "Simulated browser restart while train-hop add-on is pending installation"
    343  );
    344  mockAboutNewTabUninit();
    345  await AddonTestUtils.promiseRestartManager();
    346  AboutNewTab.init();
    347 
    348  await asyncAssertNewTabAddon({
    349    locationName: PROFILE_LOCATION_NAME,
    350    version: updateAddonVersion,
    351  });
    352  const trainhopAddonPolicy = WebExtensionPolicy.getByID(BUILTIN_ADDON_ID);
    353  Assert.equal(
    354    trainhopAddonPolicy?.extension?.version,
    355    updateAddonVersion,
    356    "Got newtab WebExtensionPolicy instance for the train-hop add-on version"
    357  );
    358  assertNewTabResourceMapping(trainhopAddonPolicy.extension.rootURI.spec);
    359 
    360  info(
    361    "Simulated browser restart with a builtin add-on version higher than the train-hop add-on version"
    362  );
    363  // Mock a builtin version with an add-on version higher than the train-hop add-on version.
    364  const fakeUpdatedBuiltinVersion = "9999.0";
    365  const restoreBuiltinAddonsSubstitution =
    366    await overrideBuiltinAddonsSubstitution(fakeUpdatedBuiltinVersion);
    367 
    368  mockAboutNewTabUninit();
    369  await AddonTestUtils.promiseRestartManager();
    370  AboutNewTab.init();
    371  assertNewTabResourceMapping();
    372  await AboutNewTabResourceMapping.updateTrainhopAddonState();
    373  // Expect the newtab xpi to have been uninstalled and the updated
    374  // builtin add-on to be the newtab add-on version becoming active.
    375  await asyncAssertNewTabAddon({
    376    locationName: BUILTIN_LOCATION_NAME,
    377    version: fakeUpdatedBuiltinVersion,
    378  });
    379  Assert.deepEqual(
    380    await AddonManager.getAllInstalls(),
    381    [],
    382    "Expect no pending install to be found"
    383  );
    384 
    385  // Cleanup
    386  mockAboutNewTabUninit();
    387  await restoreBuiltinAddonsSubstitution();
    388  await AddonTestUtils.promiseRestartManager();
    389  AboutNewTab.init();
    390  assertNewTabResourceMapping();
    391  await asyncAssertNewTabAddon({
    392    locationName: BUILTIN_LOCATION_NAME,
    393    version: BUILTIN_ADDON_VERSION,
    394  });
    395  await nimbusFeatureCleanup();
    396 
    397  async function overrideBuiltinAddonsSubstitution(updatedBuiltinVersion) {
    398    const { ExtensionTestCommon } = ChromeUtils.importESModule(
    399      "resource://testing-common/ExtensionTestCommon.sys.mjs"
    400    );
    401    const fakeBuiltinAddonsDir = AddonTestUtils.tempDir.clone();
    402    fakeBuiltinAddonsDir.append("builtin-addons-override");
    403    const addonDir = fakeBuiltinAddonsDir.clone();
    404    addonDir.append("newtab");
    405    await AddonTestUtils.promiseWriteFilesToDir(
    406      addonDir.path,
    407      ExtensionTestCommon.generateFiles({
    408        manifest: {
    409          version: updatedBuiltinVersion,
    410          browser_specific_settings: {
    411            gecko: { id: BUILTIN_ADDON_ID },
    412          },
    413        },
    414      })
    415    );
    416    const resProto = Cc[
    417      "@mozilla.org/network/protocol;1?name=resource"
    418    ].getService(Ci.nsIResProtocolHandler);
    419    let defaultBuiltinAddonsSubstitution =
    420      resProto.getSubstitution("builtin-addons");
    421    resProto.setSubstitutionWithFlags(
    422      "builtin-addons",
    423      Services.io.newFileURI(fakeBuiltinAddonsDir),
    424      Ci.nsISubstitutingProtocolHandler.ALLOW_CONTENT_ACCESS
    425    );
    426 
    427    // Verify we mocked an updated newtab builtin manifest as expected.
    428    const mockedManifest = await fetch(
    429      "resource://builtin-addons/newtab/manifest.json"
    430    ).then(r => r.json());
    431    Assert.equal(
    432      mockedManifest.version,
    433      fakeUpdatedBuiltinVersion,
    434      "Got the expected manifest version in the mocked builtin add-on manifest"
    435    );
    436 
    437    // Update built_in_addons.json accordingly.
    438    await overrideBuiltinsNewTabVersion(updatedBuiltinVersion);
    439 
    440    return async () => {
    441      await overrideBuiltinsNewTabVersion(BUILTIN_ADDON_VERSION);
    442      resProto.setSubstitutionWithFlags(
    443        "builtin-addons",
    444        defaultBuiltinAddonsSubstitution,
    445        Ci.nsISubstitutingProtocolHandler.ALLOW_CONTENT_ACCESS
    446      );
    447      fakeBuiltinAddonsDir.remove(true);
    448    };
    449  }
    450 
    451  async function overrideBuiltinsNewTabVersion(addon_version) {
    452    // Override newtab builtin version in built_in_addons.json metadata.
    453    const builtinsConfig = await fetch(
    454      "chrome://browser/content/built_in_addons.json"
    455    ).then(res => res.json());
    456    await AddonTestUtils.overrideBuiltIns({
    457      system: [],
    458      builtins: builtinsConfig.builtins
    459        .filter(entry => entry.addon_id === BUILTIN_ADDON_ID)
    460        .map(entry => {
    461          entry.addon_version = addon_version;
    462          return entry;
    463        }),
    464    });
    465  }
    466 });
    467 
    468 add_task(async function test_nonsystem_xpi_uninstalled() {
    469  let sandbox = sinon.createSandbox();
    470 
    471  // Sanity check (verifies builtin add-on resources have been mapped).
    472  assertNewTabResourceMapping();
    473 
    474  const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.123`;
    475  const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({
    476    updateAddonVersion,
    477  });
    478  assertTrainhopAddonVersionPref(updateAddonVersion);
    479  await AboutNewTabResourceMapping.updateTrainhopAddonState();
    480 
    481  info("Simulated restart after train-hop add-on version install pending");
    482  mockAboutNewTabUninit();
    483  await AddonTestUtils.promiseRestartManager();
    484  AboutNewTab.init();
    485 
    486  await asyncAssertNewTabAddon({
    487    locationName: PROFILE_LOCATION_NAME,
    488    version: updateAddonVersion,
    489  });
    490 
    491  // Install non-system signed newtab XPI (Expected to be installed
    492  // right away because the fake train-hop add-on version doesn't
    493  // have an onUpdateAvailable listener).
    494  const xpiVersion = `${BUILTIN_ADDON_VERSION}.456`;
    495  let extension = await ExtensionTestUtils.loadExtension({
    496    useAddonManager: "permanent",
    497    manifest: {
    498      version: xpiVersion,
    499      browser_specific_settings: {
    500        gecko: { id: BUILTIN_ADDON_ID },
    501      },
    502    },
    503  });
    504  const oldUsePrivilegedSignatures = AddonTestUtils.usePrivilegedSignatures;
    505  AddonTestUtils.usePrivilegedSignatures = false;
    506  await extension.startup();
    507  AddonTestUtils.usePrivilegedSignatures = oldUsePrivilegedSignatures;
    508 
    509  let addon = await asyncAssertNewTabAddon({
    510    locationName: PROFILE_LOCATION_NAME,
    511    version: xpiVersion,
    512  });
    513  Assert.deepEqual(
    514    addon.signedState,
    515    AddonManager.SIGNEDSTATE_SIGNED,
    516    "Got the expected signedState for the installed XPI version"
    517  );
    518 
    519  mockAboutNewTabUninit();
    520  await AddonTestUtils.promiseRestartManager();
    521  AboutNewTab.init();
    522  assertNewTabResourceMapping();
    523 
    524  sandbox.stub(AboutHomeStartupCache, "clearCacheAndUninit").returns();
    525  await AboutNewTabResourceMapping.updateTrainhopAddonState();
    526  Assert.ok(
    527    AboutHomeStartupCache.clearCacheAndUninit.called,
    528    "Uninstalling caused the startup cache to be cleared."
    529  );
    530 
    531  // Expect the newtab xpi to have been uninstalled and the updated
    532  // builtin add-on to be the newtab add-on version becoming active.
    533  await asyncAssertNewTabAddon({
    534    locationName: BUILTIN_LOCATION_NAME,
    535    version: BUILTIN_ADDON_VERSION,
    536  });
    537  // Along with uninstalling the non-system signed xpi we expect the
    538  // call to updateTrainhopAddonState to be installing the original
    539  // train-hop add-on version again.
    540  const { pendingInstall } = await asyncAssertNimbusTrainhopAddonStaged({
    541    updateAddonVersion,
    542  });
    543  await cancelPendingInstall(pendingInstall);
    544 
    545  await nimbusFeatureCleanup();
    546  sandbox.restore();
    547 });