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 }