test_ext_chrome_settings_overrides_update.js (22513B)
1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 /* vim: set sts=2 sw=2 et tw=80: */ 3 "use strict"; 4 5 const { AddonTestUtils } = ChromeUtils.importESModule( 6 "resource://testing-common/AddonTestUtils.sys.mjs" 7 ); 8 9 ChromeUtils.defineESModuleGetters(this, { 10 AddonManager: "resource://gre/modules/AddonManager.sys.mjs", 11 HomePage: "resource:///modules/HomePage.sys.mjs", 12 RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", 13 sinon: "resource://testing-common/Sinon.sys.mjs", 14 }); 15 16 AddonTestUtils.init(this); 17 AddonTestUtils.overrideCertDB(); 18 19 AddonTestUtils.createAppInfo( 20 "xpcshell@tests.mozilla.org", 21 "XPCShell", 22 "1", 23 "42" 24 ); 25 26 // Similar to TestUtils.topicObserved, but returns a deferred promise that 27 // can be resolved 28 function topicObservable(topic, checkFn) { 29 let deferred = Promise.withResolvers(); 30 function observer(subject, topic, data) { 31 try { 32 if (checkFn && !checkFn(subject, data)) { 33 return; 34 } 35 deferred.resolve([subject, data]); 36 } catch (ex) { 37 deferred.reject(ex); 38 } 39 } 40 deferred.promise.finally(() => { 41 Services.obs.removeObserver(observer, topic); 42 checkFn = null; 43 }); 44 Services.obs.addObserver(observer, topic); 45 46 return deferred; 47 } 48 49 async function setupRemoteSettings() { 50 const settings = await RemoteSettings("hijack-blocklists"); 51 sinon.stub(settings, "get").returns([ 52 { 53 id: "homepage-urls", 54 matches: ["ignore=me"], 55 _status: "synced", 56 }, 57 ]); 58 } 59 60 function promisePrefChanged(expectedValue) { 61 return TestUtils.waitForPrefChange("browser.startup.homepage", value => 62 value.endsWith(expectedValue) 63 ); 64 } 65 66 add_task(async function setup() { 67 await AddonTestUtils.promiseStartupManager(); 68 await setupRemoteSettings(); 69 }); 70 71 add_task(async function test_overrides_update_removal() { 72 /* This tests the scenario where the manifest key for homepage and/or 73 * search_provider are removed between updates and therefore the 74 * settings are expected to revert. It also tests that an extension 75 * can make a builtin extension the default search without user 76 * interaction. */ 77 78 const EXTENSION_ID = "test_overrides_update@tests.mozilla.org"; 79 const HOMEPAGE_URI = "webext-homepage-1.html"; 80 81 let extensionInfo = { 82 useAddonManager: "permanent", 83 manifest: { 84 version: "1.0", 85 browser_specific_settings: { 86 gecko: { 87 id: EXTENSION_ID, 88 }, 89 }, 90 chrome_settings_overrides: { 91 homepage: HOMEPAGE_URI, 92 search_provider: { 93 name: "DuckDuckGo", 94 search_url: "https://example.com/?q={searchTerms}", 95 is_default: true, 96 }, 97 }, 98 }, 99 }; 100 let extension = ExtensionTestUtils.loadExtension(extensionInfo); 101 102 let defaultHomepageURL = HomePage.get(); 103 let defaultEngineName = (await Services.search.getDefault()).name; 104 Assert.notStrictEqual( 105 defaultEngineName, 106 "DuckDuckGo", 107 "Default engine is not DuckDuckGo." 108 ); 109 110 let prefPromise = promisePrefChanged(HOMEPAGE_URI); 111 112 // When an addon is installed that overrides a config engine 113 // that is the default, we do not prompt for default. 114 let deferredPrompt = topicObservable( 115 "webextension-defaultsearch-prompt", 116 subject => { 117 if (subject.wrappedJSObject.id == extension.id) { 118 ok(false, "default override should not prompt"); 119 } 120 } 121 ); 122 123 await Promise.race([extension.startup(), deferredPrompt.promise]); 124 deferredPrompt.resolve(); 125 await AddonTestUtils.waitForSearchProviderStartup(extension); 126 await prefPromise; 127 128 equal( 129 extension.version, 130 "1.0", 131 "The installed addon has the expected version." 132 ); 133 ok( 134 HomePage.get().endsWith(HOMEPAGE_URI), 135 "Home page url is overridden by the extension." 136 ); 137 equal( 138 (await Services.search.getDefault()).name, 139 "DuckDuckGo", 140 "Builtin default engine was set default by extension" 141 ); 142 143 extensionInfo.manifest = { 144 version: "2.0", 145 browser_specific_settings: { 146 gecko: { 147 id: EXTENSION_ID, 148 }, 149 }, 150 }; 151 152 prefPromise = promisePrefChanged(defaultHomepageURL); 153 await extension.upgrade(extensionInfo); 154 await prefPromise; 155 156 equal( 157 extension.version, 158 "2.0", 159 "The updated addon has the expected version." 160 ); 161 equal( 162 HomePage.get(), 163 defaultHomepageURL, 164 "Home page url reverted to the default after update." 165 ); 166 equal( 167 (await Services.search.getDefault()).name, 168 defaultEngineName, 169 "Default engine reverted to the default after update." 170 ); 171 172 await extension.unload(); 173 }); 174 175 add_task(async function test_overrides_update_adding() { 176 /* This tests the scenario where an addon adds support for 177 * a homepage or search service when upgrading. Neither 178 * should override existing entries for those when added 179 * in an upgrade. Also, a search_provider being added 180 * with is_default should not prompt the user or override 181 * the current default engine. */ 182 183 const EXTENSION_ID = "test_overrides_update@tests.mozilla.org"; 184 const HOMEPAGE_URI = "webext-homepage-1.html"; 185 186 let extensionInfo = { 187 useAddonManager: "permanent", 188 manifest: { 189 version: "1.0", 190 browser_specific_settings: { 191 gecko: { 192 id: EXTENSION_ID, 193 }, 194 }, 195 }, 196 }; 197 let extension = ExtensionTestUtils.loadExtension(extensionInfo); 198 199 let defaultHomepageURL = HomePage.get(); 200 let defaultEngineName = (await Services.search.getDefault()).name; 201 Assert.notStrictEqual( 202 defaultEngineName, 203 "DuckDuckGo", 204 "Home page url is not DuckDuckGo." 205 ); 206 207 await extension.startup(); 208 209 equal( 210 extension.version, 211 "1.0", 212 "The installed addon has the expected version." 213 ); 214 equal( 215 HomePage.get(), 216 defaultHomepageURL, 217 "Home page url is the default after startup." 218 ); 219 equal( 220 (await Services.search.getDefault()).name, 221 defaultEngineName, 222 "Default engine is the default after startup." 223 ); 224 225 extensionInfo.manifest = { 226 version: "2.0", 227 browser_specific_settings: { 228 gecko: { 229 id: EXTENSION_ID, 230 }, 231 }, 232 chrome_settings_overrides: { 233 homepage: HOMEPAGE_URI, 234 search_provider: { 235 name: "DuckDuckGo", 236 search_url: "https://example.com/?q={searchTerms}", 237 is_default: true, 238 }, 239 }, 240 }; 241 242 let prefPromise = promisePrefChanged(HOMEPAGE_URI); 243 244 let deferredUpgradePrompt = topicObservable( 245 "webextension-defaultsearch-prompt", 246 subject => { 247 if (subject.wrappedJSObject.id == extension.id) { 248 ok(false, "should not prompt on update"); 249 } 250 } 251 ); 252 253 await Promise.race([ 254 extension.upgrade(extensionInfo), 255 deferredUpgradePrompt.promise, 256 ]); 257 deferredUpgradePrompt.resolve(); 258 await AddonTestUtils.waitForSearchProviderStartup(extension); 259 await prefPromise; 260 261 equal( 262 extension.version, 263 "2.0", 264 "The updated addon has the expected version." 265 ); 266 ok( 267 HomePage.get().endsWith(HOMEPAGE_URI), 268 "Home page url is overridden by the extension during upgrade." 269 ); 270 // An upgraded extension adding a search engine cannot override 271 // the default engine. 272 equal( 273 (await Services.search.getDefault()).name, 274 defaultEngineName, 275 "Default engine is still the default after startup." 276 ); 277 278 await extension.unload(); 279 }); 280 281 add_task(async function test_overrides_update_homepage_change() { 282 /* This tests the scenario where an addon changes 283 * a homepage url when upgrading. */ 284 285 const EXTENSION_ID = "test_overrides_update@tests.mozilla.org"; 286 const HOMEPAGE_URI = "webext-homepage-1.html"; 287 const HOMEPAGE_URI_2 = "webext-homepage-2.html"; 288 289 let extensionInfo = { 290 useAddonManager: "permanent", 291 manifest: { 292 version: "1.0", 293 browser_specific_settings: { 294 gecko: { 295 id: EXTENSION_ID, 296 }, 297 }, 298 chrome_settings_overrides: { 299 homepage: HOMEPAGE_URI, 300 }, 301 }, 302 }; 303 let extension = ExtensionTestUtils.loadExtension(extensionInfo); 304 305 let prefPromise = promisePrefChanged(HOMEPAGE_URI); 306 await extension.startup(); 307 await prefPromise; 308 309 equal( 310 extension.version, 311 "1.0", 312 "The installed addon has the expected version." 313 ); 314 ok( 315 HomePage.get().endsWith(HOMEPAGE_URI), 316 "Home page url is the extension url after startup." 317 ); 318 319 extensionInfo.manifest = { 320 version: "2.0", 321 browser_specific_settings: { 322 gecko: { 323 id: EXTENSION_ID, 324 }, 325 }, 326 chrome_settings_overrides: { 327 homepage: HOMEPAGE_URI_2, 328 }, 329 }; 330 331 prefPromise = promisePrefChanged(HOMEPAGE_URI_2); 332 await extension.upgrade(extensionInfo); 333 await prefPromise; 334 335 equal( 336 extension.version, 337 "2.0", 338 "The updated addon has the expected version." 339 ); 340 ok( 341 HomePage.get().endsWith(HOMEPAGE_URI_2), 342 "Home page url is by the extension after upgrade." 343 ); 344 345 await extension.unload(); 346 }); 347 348 async function withHandlingDefaultSearchPrompt({ extensionId, respond }, cb) { 349 const promptResponseHandled = TestUtils.topicObserved( 350 "webextension-defaultsearch-prompt-response" 351 ); 352 const prompted = TestUtils.topicObserved( 353 "webextension-defaultsearch-prompt", 354 subject => { 355 if (subject.wrappedJSObject.id == extensionId) { 356 return subject.wrappedJSObject.respond(respond); 357 } 358 } 359 ); 360 361 await Promise.all([cb(), prompted, promptResponseHandled]); 362 } 363 364 async function assertUpdateDoNotPrompt(extension, updateExtensionInfo) { 365 let deferredUpgradePrompt = topicObservable( 366 "webextension-defaultsearch-prompt", 367 subject => { 368 if (subject.wrappedJSObject.id == extension.id) { 369 ok(false, "should not prompt on update"); 370 } 371 } 372 ); 373 374 await Promise.race([ 375 extension.upgrade(updateExtensionInfo), 376 deferredUpgradePrompt.promise, 377 ]); 378 deferredUpgradePrompt.resolve(); 379 380 await AddonTestUtils.waitForSearchProviderStartup(extension); 381 382 equal( 383 extension.version, 384 updateExtensionInfo.manifest.version, 385 "The updated addon has the expected version." 386 ); 387 } 388 389 add_task(async function test_default_search_prompts() { 390 /* This tests the scenario where an addon did not gain 391 * default search during install, and later upgrades. 392 * The addon should not gain default in updates. 393 * If the addon is disabled, it should prompt again when 394 * enabled. 395 */ 396 397 const EXTENSION_ID = "test_default_update@tests.mozilla.org"; 398 399 let extensionInfo = { 400 useAddonManager: "permanent", 401 manifest: { 402 version: "1.0", 403 browser_specific_settings: { 404 gecko: { 405 id: EXTENSION_ID, 406 }, 407 }, 408 chrome_settings_overrides: { 409 search_provider: { 410 name: "Example", 411 search_url: "https://example.com/?q={searchTerms}", 412 is_default: true, 413 }, 414 }, 415 }, 416 }; 417 418 let extension = ExtensionTestUtils.loadExtension(extensionInfo); 419 420 let defaultEngineName = (await Services.search.getDefault()).name; 421 Assert.notStrictEqual(defaultEngineName, "Example", "Search is not Example."); 422 423 // Mock a response from the default search prompt where we 424 // say no to setting this as the default when installing. 425 await withHandlingDefaultSearchPrompt( 426 { extensionId: EXTENSION_ID, respond: false }, 427 () => extension.startup() 428 ); 429 430 equal( 431 extension.version, 432 "1.0", 433 "The installed addon has the expected version." 434 ); 435 equal( 436 (await Services.search.getDefault()).name, 437 defaultEngineName, 438 "Default engine is the default after startup." 439 ); 440 441 info( 442 "Verify that updating the extension does not prompt and does not take over the default engine" 443 ); 444 445 extensionInfo.manifest.version = "2.0"; 446 await assertUpdateDoNotPrompt(extension, extensionInfo); 447 equal( 448 (await Services.search.getDefault()).name, 449 defaultEngineName, 450 "Default engine is still the default after update." 451 ); 452 453 info("Verify that disable/enable the extension does prompt the user"); 454 455 let addon = await AddonManager.getAddonByID(EXTENSION_ID); 456 457 await withHandlingDefaultSearchPrompt( 458 { extensionId: EXTENSION_ID, respond: false }, 459 async () => { 460 await addon.disable(); 461 await addon.enable(); 462 } 463 ); 464 465 // we still said no. 466 equal( 467 (await Services.search.getDefault()).name, 468 defaultEngineName, 469 "Default engine is the default after being disabling/enabling." 470 ); 471 472 await extension.unload(); 473 }); 474 475 async function test_default_search_on_updating_addons_installed_before_bug1757760({ 476 builtinAsInitialDefault, 477 }) { 478 /* This tests covers a scenario similar to the previous test but with an extension-settings.json file 479 content like the one that would be available in the profile if the add-on was installed on firefox 480 versions that didn't include the changes from Bug 1757760 (See Bug 1767550). 481 */ 482 483 const EXTENSION_ID = `test_old_addon@tests.mozilla.org`; 484 const EXTENSION_ID2 = `test_old_addon2@tests.mozilla.org`; 485 486 const extensionInfo = { 487 useAddonManager: "permanent", 488 manifest: { 489 version: "1.1", 490 browser_specific_settings: { 491 gecko: { 492 id: EXTENSION_ID, 493 }, 494 }, 495 chrome_settings_overrides: { 496 search_provider: { 497 name: "Test SearchEngine", 498 search_url: "https://example.com/?q={searchTerms}", 499 is_default: true, 500 }, 501 }, 502 }, 503 }; 504 505 const extensionInfo2 = { 506 useAddonManager: "permanent", 507 manifest: { 508 version: "1.2", 509 browser_specific_settings: { 510 gecko: { 511 id: EXTENSION_ID2, 512 }, 513 }, 514 chrome_settings_overrides: { 515 search_provider: { 516 name: "Test SearchEngine2", 517 search_url: "https://example.com/?q={searchTerms}", 518 is_default: true, 519 }, 520 }, 521 }, 522 }; 523 524 const { ExtensionSettingsStore } = ChromeUtils.importESModule( 525 "resource://gre/modules/ExtensionSettingsStore.sys.mjs" 526 ); 527 528 async function assertExtensionSettingsStore( 529 extensionInfo, 530 expectedLevelOfControl 531 ) { 532 const { id } = extensionInfo.manifest.browser_specific_settings.gecko; 533 info(`Asserting ExtensionSettingsStore for ${id}`); 534 const item = ExtensionSettingsStore.getSetting( 535 "default_search", 536 "defaultSearch", 537 id 538 ); 539 equal( 540 item.value, 541 extensionInfo.manifest.chrome_settings_overrides.search_provider.name, 542 "Got the expected item returned by ExtensionSettingsStore.getSetting" 543 ); 544 const control = await ExtensionSettingsStore.getLevelOfControl( 545 id, 546 "default_search", 547 "defaultSearch" 548 ); 549 equal( 550 control, 551 expectedLevelOfControl, 552 `Got expected levelOfControl for ${id}` 553 ); 554 } 555 556 info("Install test extensions without opt-in to the related search engines"); 557 558 let extension = ExtensionTestUtils.loadExtension(extensionInfo); 559 let extension2 = ExtensionTestUtils.loadExtension(extensionInfo2); 560 561 // Mock a response from the default search prompt where we 562 // say no to setting this as the default when installing. 563 await withHandlingDefaultSearchPrompt( 564 { extensionId: EXTENSION_ID, respond: false }, 565 () => extension.startup() 566 ); 567 568 equal( 569 extension.version, 570 "1.1", 571 "first installed addon has the expected version." 572 ); 573 574 // Mock a response from the default search prompt where we 575 // say no to setting this as the default when installing. 576 await withHandlingDefaultSearchPrompt( 577 { extensionId: EXTENSION_ID2, respond: false }, 578 () => extension2.startup() 579 ); 580 581 equal( 582 extension2.version, 583 "1.2", 584 "second installed addon has the expected version." 585 ); 586 587 info("Setup preconditions (set the initial default search engine)"); 588 589 // Sanity check to be sure the initial engine expected as precondition 590 // for the scenario covered by the current test case. 591 let initialEngine; 592 if (builtinAsInitialDefault) { 593 initialEngine = Services.search.appDefaultEngine; 594 } else { 595 initialEngine = Services.search.getEngineByName( 596 extensionInfo.manifest.chrome_settings_overrides.search_provider.name 597 ); 598 } 599 await Services.search.setDefault( 600 initialEngine, 601 Ci.nsISearchService.CHANGE_REASON_UNKNOWN 602 ); 603 604 let defaultEngineName = (await Services.search.getDefault()).name; 605 Assert.equal( 606 defaultEngineName, 607 initialEngine.name, 608 `initial default search engine expected to be ${ 609 builtinAsInitialDefault ? "app-provided" : EXTENSION_ID 610 }` 611 ); 612 Assert.notEqual( 613 defaultEngineName, 614 extensionInfo2.manifest.chrome_settings_overrides.search_provider.name, 615 "initial default search engine name should not be the same as the second extension search_provider" 616 ); 617 618 equal( 619 (await Services.search.getDefault()).name, 620 initialEngine.name, 621 `Default engine should still be set to the ${ 622 builtinAsInitialDefault ? "app-provided" : EXTENSION_ID 623 }.` 624 ); 625 626 // Mock an update from settings stored as in an older Firefox version where Bug 1757760 was not landed yet. 627 info( 628 "Setup preconditions (inject mock extension-settings.json data and assert on the expected setting and levelOfControl)" 629 ); 630 631 let addon = await AddonManager.getAddonByID(EXTENSION_ID); 632 let addon2 = await AddonManager.getAddonByID(EXTENSION_ID2); 633 634 const extensionSettingsData = { 635 version: 2, 636 url_overrides: {}, 637 prefs: {}, 638 homepageNotification: {}, 639 tabHideNotification: {}, 640 default_search: { 641 defaultSearch: { 642 initialValue: Services.search.appDefaultEngine.name, 643 precedenceList: [ 644 { 645 id: EXTENSION_ID2, 646 // The install dates are used in ExtensionSettingsStore.getLevelOfControl 647 // and to recreate the expected preconditions the last extension installed 648 // should have a installDate timestamp > then the first one. 649 installDate: addon2.installDate.getTime() + 1000, 650 value: 651 extensionInfo2.manifest.chrome_settings_overrides.search_provider 652 .name, 653 // When an addon with a default search engine override is installed in Firefox versions 654 // without the changes landed from Bug 1757760, `enabled` will be set to true in all cases 655 // (Prompt never answered, or when No or Yes is selected by the user). 656 enabled: true, 657 }, 658 { 659 id: EXTENSION_ID, 660 installDate: addon.installDate.getTime(), 661 value: 662 extensionInfo.manifest.chrome_settings_overrides.search_provider 663 .name, 664 enabled: true, 665 }, 666 ], 667 }, 668 }, 669 newTabNotification: {}, 670 commands: {}, 671 }; 672 673 const file = Services.dirsvc.get("ProfD", Ci.nsIFile); 674 file.append("extension-settings.json"); 675 676 info(`writing mock settings data into ${file.path}`); 677 await IOUtils.writeJSON(file.path, extensionSettingsData); 678 await ExtensionSettingsStore._reloadFile(false); 679 680 equal( 681 (await Services.search.getDefault()).name, 682 initialEngine.name, 683 "Default engine is still set to the initial one." 684 ); 685 686 // The following assertions verify that the migration applied from ExtensionSettingsStore 687 // fixed the inconsistent state and kept the search engine unchanged. 688 // 689 // - With the fixed settings we expect both to be resolved to "controllable_by_this_extension". 690 // - Without the fix applied during the migration the levelOfControl resolved would be: 691 // - for the last installed: "controlled_by_this_extension" 692 // - for the first installed: "controlled_by_other_extensions" 693 await assertExtensionSettingsStore( 694 extensionInfo2, 695 "controlled_by_this_extension" 696 ); 697 await assertExtensionSettingsStore( 698 extensionInfo, 699 "controlled_by_other_extensions" 700 ); 701 702 info( 703 "Verify that updating the extension does not prompt and does not take over the default engine" 704 ); 705 706 extensionInfo2.manifest.version = "2.2"; 707 await assertUpdateDoNotPrompt(extension2, extensionInfo2); 708 709 extensionInfo.manifest.version = "2.1"; 710 await assertUpdateDoNotPrompt(extension, extensionInfo); 711 712 equal( 713 (await Services.search.getDefault()).name, 714 initialEngine.name, 715 "Default engine is still the same after updating both the test extensions." 716 ); 717 718 // After both the extensions have been updated and their inconsistent state 719 // updated internally, both extensions should have levelOfControl "controllable_*". 720 await assertExtensionSettingsStore( 721 extensionInfo2, 722 "controllable_by_this_extension" 723 ); 724 await assertExtensionSettingsStore( 725 extensionInfo, 726 // We expect levelOfControl to be controlled_by_this_extension if the test case 727 // is expecting the third party extension to stay set as default. 728 builtinAsInitialDefault 729 ? "controllable_by_this_extension" 730 : "controlled_by_this_extension" 731 ); 732 733 info("Verify that disable/enable the extension does prompt the user"); 734 735 await withHandlingDefaultSearchPrompt( 736 { extensionId: EXTENSION_ID2, respond: false }, 737 async () => { 738 await addon2.disable(); 739 await addon2.enable(); 740 } 741 ); 742 743 // we said no. 744 equal( 745 (await Services.search.getDefault()).name, 746 initialEngine.name, 747 `Default engine should still be the same after disabling/enabling ${EXTENSION_ID2}.` 748 ); 749 750 await withHandlingDefaultSearchPrompt( 751 { extensionId: EXTENSION_ID, respond: false }, 752 async () => { 753 await addon.disable(); 754 await addon.enable(); 755 } 756 ); 757 758 // we said no. 759 equal( 760 (await Services.search.getDefault()).name, 761 Services.search.appDefaultEngine.name, 762 `Default engine should be set to the app default after disabling/enabling ${EXTENSION_ID}.` 763 ); 764 765 await withHandlingDefaultSearchPrompt( 766 { extensionId: EXTENSION_ID, respond: true }, 767 async () => { 768 await addon.disable(); 769 await addon.enable(); 770 } 771 ); 772 773 // we responded yes. 774 equal( 775 (await Services.search.getDefault()).name, 776 extensionInfo.manifest.chrome_settings_overrides.search_provider.name, 777 "Default engine should be set to the one opted-in from the last prompt." 778 ); 779 780 await extension.unload(); 781 await extension2.unload(); 782 } 783 784 add_task(function test_builtin_default_search_after_updating_old_addons() { 785 return test_default_search_on_updating_addons_installed_before_bug1757760({ 786 builtinAsInitialDefault: true, 787 }); 788 }); 789 790 add_task(function test_third_party_default_search_after_updating_old_addons() { 791 return test_default_search_on_updating_addons_installed_before_bug1757760({ 792 builtinAsInitialDefault: false, 793 }); 794 });