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 });