tor-browser

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

test_nimbus_newtabTrainhopAddon_firstStartup.js (13656B)


      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 const { FirstStartup } = ChromeUtils.importESModule(
     17  "resource://gre/modules/FirstStartup.sys.mjs"
     18 );
     19 const { updateAppInfo } = ChromeUtils.importESModule(
     20  "resource://testing-common/AppInfo.sys.mjs"
     21 );
     22 
     23 const PREF_CATEGORY_TASKS = "first-startup.category-tasks-enabled";
     24 const CATEGORY_NAME = "first-startup-new-profile";
     25 
     26 add_setup(async () => {
     27  Services.fog.testResetFOG();
     28  updateAppInfo();
     29 });
     30 
     31 /**
     32 * Test that AboutNewTabResourceMapping has a first-startup-new-profile
     33 * category entry registered for it for the
     34 * AboutNewTabResourceMapping.firstStartupNewProfile method.
     35 */
     36 add_task(async function test_is_firstStartupNewProfile_registered() {
     37  const entry = Services.catMan.getCategoryEntry(
     38    CATEGORY_NAME,
     39    "resource:///modules/AboutNewTabResourceMapping.sys.mjs"
     40  );
     41  Assert.ok(
     42    entry,
     43    "An entry should exist for resource:///modules/AboutNewTabResourceMapping.sys.mjs"
     44  );
     45  Assert.equal(
     46    entry,
     47    "AboutNewTabResourceMapping.firstStartupNewProfile",
     48    "Entry value should point to the `firstStartupNewProfile` method"
     49  );
     50 });
     51 
     52 /**
     53 * Test that the firstStartupNewProfile hook gets called during FirstStartup
     54 * and performs a restartless install of a train-hop add-on when Nimbus is
     55 * configured with one.
     56 */
     57 add_task(
     58  { skip_if: () => !AppConstants.MOZ_NORMANDY },
     59  async function test_firstStartup_trainhop_restartless_install() {
     60    // Enable category tasks for first startup
     61    Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, true);
     62    FirstStartup.resetForTesting();
     63 
     64    // Reset AboutNewTabResourceMapping state so firstStartupNewProfile can run
     65    mockAboutNewTabUninit();
     66 
     67    // Sanity check - verify built-in add-on resources have been mapped
     68    assertNewTabResourceMapping();
     69    await asyncAssertNewTabAddon({
     70      locationName: BUILTIN_LOCATION_NAME,
     71    });
     72    assertTrainhopAddonNimbusExposure({ expectedExposure: false });
     73 
     74    const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.123`;
     75 
     76    const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({
     77      updateAddonVersion,
     78    });
     79    assertTrainhopAddonVersionPref(updateAddonVersion);
     80 
     81    // Track whether firstStartupNewProfile was called
     82    let sandbox = sinon.createSandbox();
     83    let firstStartupNewProfileSpy = sandbox.spy(
     84      AboutNewTabResourceMapping,
     85      "firstStartupNewProfile"
     86    );
     87    let aboutHomeStartupClearCacheStub = sandbox.stub(
     88      AboutHomeStartupCache,
     89      "clearCacheAndUninit"
     90    );
     91 
     92    let submissionPromise = new Promise(resolve => {
     93      GleanPings.firstStartup.testBeforeNextSubmit(() => {
     94        Assert.equal(FirstStartup.state, FirstStartup.SUCCESS);
     95        resolve();
     96      });
     97    });
     98 
     99    // Run FirstStartup which should trigger our category hook
    100    FirstStartup.init(true /* newProfile */);
    101 
    102    await submissionPromise;
    103 
    104    Assert.ok(
    105      firstStartupNewProfileSpy.calledOnce,
    106      "firstStartupNewProfile should have been called"
    107    );
    108    Assert.ok(
    109      aboutHomeStartupClearCacheStub.calledOnce,
    110      "AboutHomeStartupCache.clearCacheAndUninit called after installing train-hop"
    111    );
    112 
    113    // The train-hop add-on should have been installed restartlessly
    114    let addon = await asyncAssertNewTabAddon({
    115      locationName: PROFILE_LOCATION_NAME,
    116      version: updateAddonVersion,
    117    });
    118 
    119    Assert.ok(addon, "Train-hop add-on should be installed");
    120 
    121    // No pending installs should remain since we did a restartless install
    122    Assert.deepEqual(
    123      await AddonManager.getAllInstalls(),
    124      [],
    125      "Expect no pending install for restartless install"
    126    );
    127 
    128    sandbox.restore();
    129 
    130    await nimbusFeatureCleanup();
    131    info(
    132      "Simulated browser restart while newtabTrainhopAddon nimbus feature is unenrolled"
    133    );
    134    mockAboutNewTabUninit();
    135    await AddonTestUtils.promiseRestartManager();
    136    AboutNewTab.init();
    137 
    138    // Expected bundled newtab resources mapping for this session.
    139    assertNewTabResourceMapping();
    140    await AboutNewTabResourceMapping.updateTrainhopAddonState();
    141    await asyncAssertNewTabAddon({
    142      locationName: BUILTIN_LOCATION_NAME,
    143      version: BUILTIN_ADDON_VERSION,
    144    });
    145 
    146    assertTrainhopAddonVersionPref("");
    147    Services.prefs.clearUserPref(PREF_CATEGORY_TASKS);
    148  }
    149 );
    150 
    151 /**
    152 * Test that if AboutNewTabResourceMapping.init() has already been called
    153 * by the time firstStartupNewProfile runs, it logs an error and exits early.
    154 * This is not an expected or realistic condition, but we cover it all the same.
    155 */
    156 add_task(
    157  { skip_if: () => !AppConstants.MOZ_NORMANDY },
    158  async function test_firstStartup_after_initialization() {
    159    // Initialize AboutNewTabResourceMapping before FirstStartup runs.
    160    AboutNewTabResourceMapping.init();
    161    Assert.ok(
    162      AboutNewTabResourceMapping.initialized,
    163      "AboutNewTabResourceMapping should be initialized"
    164    );
    165 
    166    Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, true);
    167    FirstStartup.resetForTesting();
    168 
    169    const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.456`;
    170 
    171    const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({
    172      updateAddonVersion,
    173    });
    174 
    175    // Track error logging
    176    let errorLogged = false;
    177    let sandbox = sinon.createSandbox();
    178    sandbox.stub(AboutNewTabResourceMapping.logger, "error").callsFake(() => {
    179      errorLogged = true;
    180    });
    181 
    182    let submissionPromise = new Promise(resolve => {
    183      GleanPings.firstStartup.testBeforeNextSubmit(() => {
    184        resolve();
    185      });
    186    });
    187 
    188    FirstStartup.init(true /* newProfile */);
    189    await submissionPromise;
    190 
    191    Assert.ok(
    192      errorLogged,
    193      "An error should have been logged when trying to run after initialization"
    194    );
    195 
    196    // The add-on should NOT have been installed since we were too late
    197    await asyncAssertNewTabAddon({
    198      locationName: BUILTIN_LOCATION_NAME,
    199      version: BUILTIN_ADDON_VERSION,
    200    });
    201 
    202    sandbox.restore();
    203    await nimbusFeatureCleanup();
    204    Services.prefs.clearUserPref(PREF_CATEGORY_TASKS);
    205  }
    206 );
    207 
    208 /**
    209 * Test that firstStartupNewProfile doesn't run when the category tasks pref
    210 * is disabled.
    211 */
    212 add_task(
    213  { skip_if: () => !AppConstants.MOZ_NORMANDY },
    214  async function test_firstStartup_category_disabled() {
    215    // Disable category tasks
    216    Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, false);
    217    FirstStartup.resetForTesting();
    218 
    219    // Reset AboutNewTabResourceMapping state
    220    mockAboutNewTabUninit();
    221 
    222    const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.789`;
    223 
    224    const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({
    225      updateAddonVersion,
    226    });
    227 
    228    let sandbox = sinon.createSandbox();
    229    let firstStartupNewProfileSpy = sandbox.spy(
    230      AboutNewTabResourceMapping,
    231      "firstStartupNewProfile"
    232    );
    233 
    234    let submissionPromise = new Promise(resolve => {
    235      GleanPings.firstStartup.testBeforeNextSubmit(() => {
    236        resolve();
    237      });
    238    });
    239 
    240    FirstStartup.init(true /* newProfile */);
    241    await submissionPromise;
    242 
    243    Assert.ok(
    244      !firstStartupNewProfileSpy.called,
    245      "firstStartupNewProfile should not have been called when pref is disabled"
    246    );
    247 
    248    // The add-on should still be the builtin version
    249    await asyncAssertNewTabAddon({
    250      locationName: BUILTIN_LOCATION_NAME,
    251      version: BUILTIN_ADDON_VERSION,
    252    });
    253 
    254    sandbox.restore();
    255    await nimbusFeatureCleanup();
    256    Services.prefs.clearUserPref(PREF_CATEGORY_TASKS);
    257  }
    258 );
    259 
    260 /**
    261 * Test that if AboutNewTabResourceMapping.init() is called after the XPI
    262 * download has started but before onInstallPostponed is called, we skip
    263 * attempting to force the restartless install and fall back to a staged
    264 * install instead.
    265 */
    266 add_task(
    267  { skip_if: () => !AppConstants.MOZ_NORMANDY },
    268  async function test_firstStartup_init_during_download() {
    269    Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, true);
    270    FirstStartup.resetForTesting();
    271 
    272    // Reset AboutNewTabResourceMapping state so firstStartupNewProfile can run
    273    mockAboutNewTabUninit();
    274 
    275    assertNewTabResourceMapping();
    276    await asyncAssertNewTabAddon({
    277      locationName: BUILTIN_LOCATION_NAME,
    278    });
    279 
    280    const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.999`;
    281 
    282    const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({
    283      updateAddonVersion,
    284    });
    285    assertTrainhopAddonVersionPref(updateAddonVersion);
    286 
    287    // Stub updateTrainhopAddonState to call init() in the middle of its execution
    288    let sandbox = sinon.createSandbox();
    289    let aboutNewTabInitSpy = sandbox.spy(AboutNewTabResourceMapping, "init");
    290 
    291    let originalUpdateTrainhopAddonState =
    292      AboutNewTabResourceMapping.updateTrainhopAddonState.bind(
    293        AboutNewTabResourceMapping
    294      );
    295 
    296    let updateTrainhopStarted = false;
    297    sandbox
    298      .stub(AboutNewTabResourceMapping, "updateTrainhopAddonState")
    299      .callsFake(async function (forceRestartlessInstall) {
    300        updateTrainhopStarted = true;
    301 
    302        // Start the update process
    303        let updatePromise = originalUpdateTrainhopAddonState(
    304          forceRestartlessInstall
    305        );
    306 
    307        // Call init immediately after starting the update, simulating
    308        // the browser window opening during the XPI download
    309        info(
    310          "Calling AboutNewTabResourceMapping.init() during updateTrainhopAddonState"
    311        );
    312        AboutNewTabResourceMapping.init();
    313 
    314        // Wait for the update to complete
    315        await updatePromise;
    316      });
    317 
    318    let submissionPromise = new Promise(resolve => {
    319      GleanPings.firstStartup.testBeforeNextSubmit(() => {
    320        Assert.equal(FirstStartup.state, FirstStartup.SUCCESS);
    321        resolve();
    322      });
    323    });
    324 
    325    FirstStartup.init(true /* newProfile */);
    326    await submissionPromise;
    327 
    328    Assert.ok(
    329      updateTrainhopStarted,
    330      "updateTrainhopAddonState should have started"
    331    );
    332    Assert.ok(
    333      aboutNewTabInitSpy.calledOnce,
    334      "AboutNewTabResourceMapping.init should have been called"
    335    );
    336 
    337    // The add-on should be staged for install, not installed restartlessly
    338    await asyncAssertNewTabAddon({
    339      locationName: BUILTIN_LOCATION_NAME,
    340      version: BUILTIN_ADDON_VERSION,
    341    });
    342 
    343    // Verify there's a pending install
    344    const pendingInstall = (await AddonManager.getAllInstalls()).find(
    345      install => install.addon.id === BUILTIN_ADDON_ID
    346    );
    347    Assert.ok(pendingInstall, "Should have a pending install");
    348    Assert.equal(
    349      pendingInstall.state,
    350      AddonManager.STATE_POSTPONED,
    351      "Install should be postponed"
    352    );
    353    Assert.equal(
    354      pendingInstall.addon.version,
    355      updateAddonVersion,
    356      "Pending install should be for the train-hop version"
    357    );
    358 
    359    // Clean up
    360    await cancelPendingInstall(pendingInstall);
    361    sandbox.restore();
    362    await nimbusFeatureCleanup();
    363    assertTrainhopAddonVersionPref("");
    364    Services.prefs.clearUserPref(PREF_CATEGORY_TASKS);
    365  }
    366 );
    367 
    368 /**
    369 * Test that the TRAINHOP_NIMBUS_FIRST_STARTUP_FEATURE_ID Nimbus feature can be
    370 * used to remotely disable the FirstStartup force-install flow.
    371 */
    372 add_task(
    373  { skip_if: () => !AppConstants.MOZ_NORMANDY },
    374  async function test_firstStartup_remote_disable() {
    375    // Enable category tasks for first startup
    376    Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, true);
    377    FirstStartup.resetForTesting();
    378 
    379    // Reset AboutNewTabResourceMapping state so firstStartupNewProfile can run
    380    mockAboutNewTabUninit();
    381 
    382    // Sanity check - verify built-in add-on resources have been mapped
    383    assertNewTabResourceMapping();
    384    await asyncAssertNewTabAddon({
    385      locationName: BUILTIN_LOCATION_NAME,
    386    });
    387    assertTrainhopAddonNimbusExposure({ expectedExposure: false });
    388 
    389    const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.123`;
    390 
    391    const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({
    392      updateAddonVersion,
    393    });
    394    assertTrainhopAddonVersionPref(updateAddonVersion);
    395 
    396    const firstStartupFeatureCleanup =
    397      await NimbusTestUtils.enrollWithFeatureConfig(
    398        {
    399          featureId: TRAINHOP_NIMBUS_FIRST_STARTUP_FEATURE_ID,
    400          value: { enabled: false },
    401        },
    402        { isRollout: true }
    403      );
    404 
    405    // Track whether firstStartupNewProfile was called
    406    let sandbox = sinon.createSandbox();
    407    let firstStartupNewProfileSpy = sandbox.spy(
    408      AboutNewTabResourceMapping,
    409      "firstStartupNewProfile"
    410    );
    411 
    412    let submissionPromise = new Promise(resolve => {
    413      GleanPings.firstStartup.testBeforeNextSubmit(() => {
    414        Assert.equal(FirstStartup.state, FirstStartup.SUCCESS);
    415        resolve();
    416      });
    417    });
    418 
    419    // Run FirstStartup which should trigger our category hook
    420    FirstStartup.init(true /* newProfile */);
    421 
    422    await submissionPromise;
    423 
    424    Assert.ok(
    425      firstStartupNewProfileSpy.calledOnce,
    426      "firstStartupNewProfile should have been called"
    427    );
    428 
    429    // The add-on should still be the builtin version
    430    await asyncAssertNewTabAddon({
    431      locationName: BUILTIN_LOCATION_NAME,
    432      version: BUILTIN_ADDON_VERSION,
    433    });
    434 
    435    sandbox.restore();
    436    await nimbusFeatureCleanup();
    437    await firstStartupFeatureCleanup();
    438    assertTrainhopAddonVersionPref("");
    439    Services.prefs.clearUserPref(PREF_CATEGORY_TASKS);
    440  }
    441 );