diff options
Diffstat (limited to 'toolkit/mozapps/extensions/test')
461 files changed, 73210 insertions, 0 deletions
diff --git a/toolkit/mozapps/extensions/test/browser/.eslintrc.js b/toolkit/mozapps/extensions/test/browser/.eslintrc.js new file mode 100644 index 0000000000..f2b9e072f9 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/.eslintrc.js @@ -0,0 +1,14 @@ +"use strict"; + +module.exports = { + env: { + webextensions: true, + }, + + rules: { + "no-unused-vars": [ + "error", + { args: "none", varsIgnorePattern: "^end_test$" }, + ], + }, +}; diff --git a/toolkit/mozapps/extensions/test/browser/addon_prefs.xhtml b/toolkit/mozapps/extensions/test/browser/addon_prefs.xhtml new file mode 100644 index 0000000000..e8cde29666 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addon_prefs.xhtml @@ -0,0 +1,6 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="addon-test-pref-window"> + <label value="Oh hai!"/> +</window> diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/manifest.mf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/manifest.mf new file mode 100644 index 0000000000..725ac8016f --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/manifest.mf @@ -0,0 +1,8 @@ +Manifest-Version: 1.0 + +Name: manifest.json +Digest-Algorithms: MD5 SHA1 SHA256 +MD5-Digest: mCLu38qfGN3trj7qKQQeEA== +SHA1-Digest: A1BaJErQY6KqnYDijP0lglrehk0= +SHA256-Digest: p2vjGP7DRqrK81NfT4LqnF7a5p8+lEuout5WLBhk9AA= + diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.rsa b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.rsa Binary files differnew file mode 100644 index 0000000000..046a0285c7 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.rsa diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.sf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.sf new file mode 100644 index 0000000000..ad4e81b574 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.sf @@ -0,0 +1,5 @@ +Signature-Version: 1.0 +MD5-Digest-Manifest: LrrwWBKNYWeVd205Hq+JwQ== +SHA1-Digest-Manifest: MeqqQN+uuf0MVesMXxbBtYN+5tU= +SHA256-Digest-Manifest: iWCxfAJX593Cn4l8R63jaQETO5HX3XOhcnpQ7nMiPlg= + diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/manifest.json b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/manifest.json new file mode 100644 index 0000000000..91012f24ed --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/manifest.json @@ -0,0 +1,12 @@ +{ + "manifest_version": 2, + + "browser_specific_settings": { + "gecko": { + "id": "dragdrop-1@tests.mozilla.org" + } + }, + + "name": "Drag Drop test 1", + "version": "1.0" +} diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/manifest.mf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/manifest.mf new file mode 100644 index 0000000000..1da3c41b23 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/manifest.mf @@ -0,0 +1,8 @@ +Manifest-Version: 1.0 + +Name: manifest.json +Digest-Algorithms: MD5 SHA1 SHA256 +MD5-Digest: 3dL7JFDBPC63pSFI5x+Z7Q== +SHA1-Digest: l1cKPyWJIYdZyvumH9VfJ6fpqVA= +SHA256-Digest: QHTjPqTMXxt5tl8zOaAzpQ8FZLqZx8LRF9LmzY+RCDQ= + diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.rsa b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.rsa Binary files differnew file mode 100644 index 0000000000..170a361620 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.rsa diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.sf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.sf new file mode 100644 index 0000000000..5301e431f7 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.sf @@ -0,0 +1,5 @@ +Signature-Version: 1.0 +MD5-Digest-Manifest: c30hzcI1ISlt46ODjVVJ2w== +SHA1-Digest-Manifest: 2yMpQHuLM0J61T7vt11NHoYI1tU= +SHA256-Digest-Manifest: qtsYxiv1zGWBp7JWxLWrIztIdxIt+i3CToReEx5fkyw= + diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/manifest.json b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/manifest.json new file mode 100644 index 0000000000..958aa03649 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/manifest.json @@ -0,0 +1,12 @@ +{ + "manifest_version": 2, + + "browser_specific_settings": { + "gecko": { + "id": "dragdrop-2@tests.mozilla.org" + } + }, + + "name": "Drag Drop test 2", + "version": "1.1" +} diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/manifest.mf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/manifest.mf new file mode 100644 index 0000000000..e508bcd22f --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/manifest.mf @@ -0,0 +1,8 @@ +Manifest-Version: 1.0 + +Name: manifest.json +Digest-Algorithms: MD5 SHA1 SHA256 +MD5-Digest: Wzo/k6fhArpFb4UB2hIKlg== +SHA1-Digest: D/WDy9api0X7OgRM6Gkvfbyzogo= +SHA256-Digest: IWBdbytHgPLtCMKKhiZ3jenxKmKiRAhh3ce8iP5AVWU= + diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.rsa b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.rsa Binary files differnew file mode 100644 index 0000000000..a026680e91 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.rsa diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.sf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.sf new file mode 100644 index 0000000000..16a1461f37 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.sf @@ -0,0 +1,5 @@ +Signature-Version: 1.0 +MD5-Digest-Manifest: ovtNeIie34gMM5l18zP2MA== +SHA1-Digest-Manifest: c5owdrvcOINxKp/HprYkWXXI/js= +SHA256-Digest-Manifest: uLPmoONlxFYxWeSTOEPJ9hN2yMDDZMJL1PoNIWcqKG4= + diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/manifest.json b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/manifest.json new file mode 100644 index 0000000000..b204e1bca7 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/manifest.json @@ -0,0 +1,13 @@ +{ + "manifest_version": 2, + + "browser_specific_settings": { + "gecko": { + "id": "dragdrop-incompat@tests.mozilla.org", + "strict_max_version": "45.0" + } + }, + + "name": "Incomatible Drag Drop test", + "version": "1.1" +} diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/manifest.mf b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/manifest.mf new file mode 100644 index 0000000000..eea5cbd501 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/manifest.mf @@ -0,0 +1,8 @@ +Manifest-Version: 1.0 + +Name: manifest.json +Digest-Algorithms: MD5 SHA1 SHA256 +MD5-Digest: b4Q2C4GsIJfRLsXc7T2ldQ== +SHA1-Digest: UG5rHxpzKmdlGrquXaguiAGDu8E= +SHA256-Digest: WZrN9SdGBux9t3lV7TVIvyUG/L1px4er2dU3TsBpC4s= + diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.rsa b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.rsa Binary files differnew file mode 100644 index 0000000000..68621e19be --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.rsa diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.sf b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.sf new file mode 100644 index 0000000000..fe6baa8dac --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.sf @@ -0,0 +1,5 @@ +Signature-Version: 1.0 +MD5-Digest-Manifest: zqRm8+jxS0iRUGWeArGkXg== +SHA1-Digest-Manifest: pa/31Ll1PYx0dPBQ6C+fd1/wJO4= +SHA256-Digest-Manifest: DJELIyswfwgeL0kaRqogXW2bzUKhn+Pickfv6WHBsW8= + diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/manifest.json b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/manifest.json new file mode 100644 index 0000000000..adc0ae09ee --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/manifest.json @@ -0,0 +1,12 @@ +{ + "manifest_version": 2, + + "browser_specific_settings": { + "gecko": { + "id": "sslinstall-1@tests.mozilla.org" + } + }, + + "name": "SSL Install Tests", + "version": "1.0" +} diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_theme/manifest.json b/toolkit/mozapps/extensions/test/browser/addons/browser_theme/manifest.json new file mode 100644 index 0000000000..7a399ddc17 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/browser_theme/manifest.json @@ -0,0 +1,22 @@ +{ + "manifest_version": 2, + + "name": "Theme test", + "version": "1.0", + + "browser_specific_settings": { + "gecko": { + "id": "theme@tests.mozilla.org" + } + }, + + "theme": { + "images": { + "theme_frame": "testImage.png" + }, + "colors": { + "frame": "#000000", + "tab_background_text": "#ffffff" + } + } +} diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/manifest.mf b/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/manifest.mf new file mode 100644 index 0000000000..a8c72c4794 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/manifest.mf @@ -0,0 +1,12 @@ +Manifest-Version: 1.0 + +Name: manifest.json +Digest-Algorithms: MD5 SHA1 +MD5-Digest: Rnoaa6yWePDor5y5/SLFaw== +SHA1-Digest: k51DtKj7bYrwkFJDdmYNDQeUBlA= + +Name: options.html +Digest-Algorithms: MD5 SHA1 +MD5-Digest: vTjxWlRpioEhTZGKTNUqIw== +SHA1-Digest: Y/mr6A34LsvekgRpdhyZRwPF1Vw= + diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.rsa b/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.rsa Binary files differnew file mode 100644 index 0000000000..8b6320adda --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.rsa diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.sf b/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.sf new file mode 100644 index 0000000000..ba5fd22caa --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.sf @@ -0,0 +1,4 @@ +Signature-Version: 1.0 +MD5-Digest-Manifest: rdmx8VMNzkZ5tRf7tt8G1w== +SHA1-Digest-Manifest: gjtTe8X9Tg46Hz2h4Tru3T02hmE= + diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed/manifest.json b/toolkit/mozapps/extensions/test/browser/addons/options_signed/manifest.json new file mode 100644 index 0000000000..e808cd5ab6 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/options_signed/manifest.json @@ -0,0 +1,11 @@ +{ + "manifest_version": 2, + + "name": "Test options_ui", + "description": "Test add-ons manager handling options_ui with no id in manifest.json", + "version": "1.2", + + "options_ui": { + "page": "options.html" + } +} diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed/options.html b/toolkit/mozapps/extensions/test/browser/addons/options_signed/options.html new file mode 100644 index 0000000000..ea804601b5 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/addons/options_signed/options.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + </head> + <body> + <div id="options-test-panel" /> + </body> +</html> diff --git a/toolkit/mozapps/extensions/test/browser/browser.toml b/toolkit/mozapps/extensions/test/browser/browser.toml new file mode 100644 index 0000000000..1daf6211f8 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser.toml @@ -0,0 +1,193 @@ +[DEFAULT] +tags = "addons" +support-files = [ + "addons/browser_dragdrop1.xpi", + "addons/browser_dragdrop1.zip", + "addons/browser_dragdrop2.xpi", + "addons/browser_dragdrop2.zip", + "addons/browser_dragdrop_incompat.xpi", + "addons/browser_installssl.xpi", + "addons/browser_theme.xpi", + "addons/options_signed.xpi", + "addons/options_signed/*", + "addon_prefs.xhtml", + "discovery/api_response.json", + "discovery/api_response_empty.json", + "discovery/small-1x1.png", + "head.js", + "redirect.sjs", + "browser_updatessl.json", + "browser_updatessl.json^headers^", + "sandboxed.html", + "sandboxed.html^headers^", + "webapi_addon_listener.html", + "webapi_checkavailable.html", + "webapi_checkchromeframe.xhtml", + "webapi_checkframed.html", + "webapi_checknavigatedwindow.html", + "!/toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi", + "!/toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi", + "!/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html", + "!/toolkit/mozapps/extensions/test/xpinstall/unsigned.xpi", + "!/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi", +] + +generated-files = [ + "addons/browser_dragdrop1.xpi", + "addons/browser_dragdrop1.zip", + "addons/browser_dragdrop2.xpi", + "addons/browser_dragdrop2.zip", + "addons/browser_dragdrop_incompat.xpi", + "addons/browser_installssl.xpi", + "addons/browser_theme.xpi", + "addons/options_signed.xpi", +] + +skip-if = [ + "os == 'linux' && asan", # Bug 1713895 - new Fission platform triage +] +prefs = [ + "dom.webmidi.enabled=true", + "midi.testing=true", +] + +["browser_AMBrowserExtensionsImport.js"] + +["browser_about_debugging_link.js"] + +["browser_addon_list_reordering.js"] + +["browser_amo_abuse_report.js"] + +["browser_bug572561.js"] + +["browser_checkAddonCompatibility.js"] + +["browser_colorwaybuiltins_migration.js"] +skip-if = [ + "app-name != 'firefox'", +] + +["browser_dragdrop.js"] +skip-if = ["true"] # Bug 1626824 + +["browser_file_xpi_no_process_switch.js"] + +["browser_globalwarnings.js"] + +["browser_gmpProvider.js"] + +["browser_history_navigation.js"] +https_first_disabled = true + +["browser_html_abuse_report.js"] +support-files = ["head_abuse_report.js"] + +["browser_html_abuse_report_dialog.js"] +support-files = ["head_abuse_report.js"] + +["browser_html_detail_permissions.js"] + +["browser_html_detail_view.js"] + +["browser_html_discover_view.js"] +https_first_disabled = true +support-files = ["head_disco.js"] + +["browser_html_discover_view_clientid.js"] + +["browser_html_discover_view_prefs.js"] + +["browser_html_list_view.js"] + +["browser_html_list_view_recommendations.js"] +skip-if = ["a11y_checks"] # Bug 1858037 to investigate intermittent a11y_checks results for .popup-notification-primary-button.primary.footer-button + +["browser_html_message_bar.js"] + +["browser_html_options_ui.js"] + +["browser_html_options_ui_in_tab.js"] + +["browser_html_pending_updates.js"] + +["browser_html_recent_updates.js"] + +["browser_html_recommendations.js"] +https_first_disabled = true + +["browser_html_scroll_restoration.js"] +skip-if = ["os == 'mac' && verify && debug"] # Bug 1850159 + +["browser_html_sitepermission_addons.js"] + +["browser_html_updates.js"] +https_first_disabled = true + +["browser_html_warning_messages.js"] + +["browser_installssl.js"] + +["browser_installtrigger_install.js"] + +["browser_local_install.js"] + +["browser_manage_shortcuts.js"] + +["browser_manage_shortcuts_hidden.js"] + +["browser_manage_shortcuts_remove.js"] + +["browser_menu_button_accessibility.js"] + +["browser_page_accessibility.js"] + +["browser_page_options_install_addon.js"] + +["browser_page_options_updates.js"] + +["browser_permission_prompt.js"] + +["browser_reinstall.js"] + +["browser_shortcuts_duplicate_check.js"] + +["browser_sidebar_categories.js"] + +["browser_sidebar_hidden_categories.js"] + +["browser_sidebar_restore_category.js"] + +["browser_subframe_install.js"] + +["browser_task_next_test.js"] + +["browser_updateid.js"] + +["browser_updatessl.js"] + +["browser_verify_l10n_strings.js"] + +["browser_webapi.js"] + +["browser_webapi_abuse_report.js"] +support-files = ["head_abuse_report.js"] + +["browser_webapi_access.js"] +https_first_disabled = true + +["browser_webapi_addon_listener.js"] + +["browser_webapi_enable.js"] + +["browser_webapi_install.js"] + +["browser_webapi_install_disabled.js"] + +["browser_webapi_theme.js"] + +["browser_webapi_uninstall.js"] + +["browser_webext_icon.js"] + +["browser_webext_incognito.js"] diff --git a/toolkit/mozapps/extensions/test/browser/browser_AMBrowserExtensionsImport.js b/toolkit/mozapps/extensions/test/browser/browser_AMBrowserExtensionsImport.js new file mode 100644 index 0000000000..654e3cd91e --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_AMBrowserExtensionsImport.js @@ -0,0 +1,220 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const { AMBrowserExtensionsImport } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +// This test verifies the global notification in `about:addons` when there are +// pending imported add-ons. The appmenu UI is covered by tests in: +// `browser/components/extensions/test/browser/browser_AMBrowserExtensionsImport.js`. + +AddonTestUtils.initMochitest(this); + +const TEST_SERVER = AddonTestUtils.createHttpServer(); + +const ADDONS = { + ext1: { + manifest: { + name: "Ext 1", + version: "1.0", + browser_specific_settings: { gecko: { id: "ff@ext-1" } }, + permissions: ["history"], + }, + }, + ext2: { + manifest: { + name: "Ext 2", + version: "1.0", + browser_specific_settings: { gecko: { id: "ff@ext-2" } }, + permissions: ["history"], + }, + }, +}; +// Populated in `setup()`. +const XPIS = {}; +// Populated in `setup()`. +const ADDON_SEARCH_RESULTS = {}; + +const mockAddonRepository = ({ addons = [] }) => { + return { + async getMappedAddons(browserID, extensionIDs) { + return Promise.resolve({ + addons, + matchedIDs: [], + unmatchedIDs: [], + }); + }, + }; +}; + +const assertWarningShown = async ( + win, + stack, + expectedWarningType = "imported-addons", + expectAction = true +) => { + Assert.equal(stack.childElementCount, 1, "expected a global warning"); + const messageBar = stack.firstElementChild; + Assert.equal( + messageBar.getAttribute("warning-type"), + expectedWarningType, + `expected a warning for ${expectedWarningType}` + ); + Assert.equal( + messageBar.getAttribute("data-l10n-id"), + `extensions-warning-${expectedWarningType}2`, + "expected correct l10n ID" + ); + await win.document.l10n.translateElements([messageBar]); + + if (expectAction) { + const button = messageBar.querySelector("button"); + Assert.equal( + button.getAttribute("action"), + expectedWarningType, + `expected a button for ${expectedWarningType}` + ); + Assert.equal( + button.getAttribute("data-l10n-id"), + `extensions-warning-${expectedWarningType}-button`, + "expected correct l10n ID on the button" + ); + await win.document.l10n.translateElements([button]); + } +}; + +add_setup(async function setup() { + for (const [name, data] of Object.entries(ADDONS)) { + XPIS[name] = AddonTestUtils.createTempWebExtensionFile(data); + TEST_SERVER.registerFile(`/addons/${name}.xpi`, XPIS[name]); + + ADDON_SEARCH_RESULTS[name] = { + id: data.manifest.browser_specific_settings.gecko.id, + name: data.name, + version: data.version, + sourceURI: Services.io.newURI( + `http://localhost:${TEST_SERVER.identity.primaryPort}/addons/${name}.xpi` + ), + icons: {}, + }; + } + + registerCleanupFunction(() => { + // Clear the add-on repository override. + AMBrowserExtensionsImport._addonRepository = null; + }); +}); + +add_task(async function test_aboutaddons_global_message() { + const browserID = "some-browser-id"; + const extensionIDs = ["ext-1", "ext-2"]; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ + addons: Object.values(ADDON_SEARCH_RESULTS), + }); + + // Global warnings should be displayed in all the `about:addons` views but + // the migration wizard links to the default view. That's why we load this + // view here, too (as opposed to, e.g., `"extensions"`). + const win = await loadInitialView(); + const stack = win.document.querySelector("global-warnings"); + + Assert.equal(stack.childElementCount, 0, "expected no global warning"); + + let promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-pending" + ); + // Start a first import... + await AMBrowserExtensionsImport.stageInstalls(browserID, extensionIDs); + await promiseTopic; + // We expect a warning about the imported add-ons to be shown. + await assertWarningShown(win, stack); + + // ...then cancel it. + promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-cancelled" + ); + await AMBrowserExtensionsImport.cancelInstalls(); + await promiseTopic; + + // At this point, the warning about the imported add-ons should be hidden. + Assert.equal(stack.childElementCount, 0, "expected no global warning"); + + // We start a second import here, then we make sure an imported-addons + // messagebar doesn't prevent the other global warning types to be shown. + promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-pending" + ); + const result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + await promiseTopic; + await assertWarningShown(win, stack); + + info("Verify safe-mode is not hidden by an imported-addons messagebar"); + stack.inSafeMode = true; + stack.refresh(); + await assertWarningShown( + win, + stack, + "safe-mode", + false /* no button expected */ + ); + stack.inSafeMode = false; + + info( + "Verify check-compatibility is not hidden by an imported-addons messagebar" + ); + AddonManager.checkCompatibility = false; + stack.refresh(); + await assertWarningShown(win, stack, "check-compatibility"); + AddonManager.checkCompatibility = true; + + info("Verify update-security is not hidden by an imported-addons messagebar"); + await SpecialPowers.pushPrefEnv({ + set: [["extensions.checkUpdateSecurity", false]], + }); + stack.refresh(); + await assertWarningShown(win, stack, "update-security"); + await SpecialPowers.popPrefEnv(); + + // After making sure the imported-addons messagebar is visible again, we + // finally complete the pending import with the UI from the global warning. + info( + "Verify pending imported addons can be completed from the messagebar action" + ); + stack.refresh(); + await assertWarningShown(win, stack, "imported-addons"); + + // Complete the installation of the add-ons by clicking on the button in the + // global warning. + promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-complete" + ); + const endedPromises = result.importedAddonIDs.map(id => + AddonTestUtils.promiseInstallEvent( + "onInstallEnded", + install => install.addon.id === id + ) + ); + stack.firstElementChild.querySelector("button").click(); + await Promise.all([...endedPromises, promiseTopic]); + + // At this point, the warning about the imported add-ons should be hidden + // because the add-ons are installed. + Assert.equal(stack.childElementCount, 0, "expected no global warning"); + + for (const id of result.importedAddonIDs) { + const addon = await AddonManager.getAddonByID(id); + Assert.ok(addon.isActive, `expected add-on "${id}" to be enabled`); + await addon.uninstall(); + } + + await closeView(win); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_about_debugging_link.js b/toolkit/mozapps/extensions/test/browser/browser_about_debugging_link.js new file mode 100644 index 0000000000..c7351f054c --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_about_debugging_link.js @@ -0,0 +1,129 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +// Allow rejections related to closing an about:debugging too soon after it has been +// just opened in a new tab and loaded. +PromiseTestUtils.allowMatchingRejectionsGlobally(/Connection closed/); + +function waitForDispatch(store, type) { + return new Promise(resolve => { + store.dispatch({ + type: "@@service/waitUntil", + predicate: action => action.type === type, + run: (dispatch, getState, action) => { + resolve(action); + }, + }); + }); +} + +/** + * Wait for all client requests to settle, meaning here that no new request has been + * dispatched after the provided delay. (NOTE: same test helper used in about:debugging tests) + */ +async function waitForRequestsToSettle(store, delay = 500) { + let hasSettled = false; + + // After each iteration of this while loop, we check is the timerPromise had the time + // to resolve or if we captured a REQUEST_*_SUCCESS action before. + while (!hasSettled) { + let timer; + + // This timer will be executed only if no REQUEST_*_SUCCESS action is dispatched + // during the delay. We consider that when no request are received for some time, it + // means there are no ongoing requests anymore. + const timerPromise = new Promise(resolve => { + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + timer = setTimeout(() => { + hasSettled = true; + resolve(); + }, delay); + }); + + // Wait either for a REQUEST_*_SUCCESS to be dispatched, or for the timer to resolve. + await Promise.race([ + waitForDispatch(store, "REQUEST_EXTENSIONS_SUCCESS"), + waitForDispatch(store, "REQUEST_TABS_SUCCESS"), + waitForDispatch(store, "REQUEST_WORKERS_SUCCESS"), + timerPromise, + ]); + + // Clear the timer to avoid setting hasSettled to true accidently unless timerPromise + // was the first to resolve. + clearTimeout(timer); + } +} + +function waitForRequestsSuccess(store) { + return Promise.all([ + waitForDispatch(store, "REQUEST_EXTENSIONS_SUCCESS"), + waitForDispatch(store, "REQUEST_TABS_SUCCESS"), + waitForDispatch(store, "REQUEST_WORKERS_SUCCESS"), + ]); +} + +add_task(async function testAboutDebugging() { + let win = await loadInitialView("extension"); + + let aboutAddonsTab = gBrowser.selectedTab; + let debugAddonsBtn = win.document.querySelector( + '#page-options [action="debug-addons"]' + ); + + // Verify the about:debugging is loaded. + info(`Check about:debugging loads`); + let loaded = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:debugging#/runtime/this-firefox", + true + ); + debugAddonsBtn.click(); + await loaded; + let aboutDebuggingTab = gBrowser.selectedTab; + const { AboutDebugging } = aboutDebuggingTab.linkedBrowser.contentWindow; + // Avoid test failures due to closing the about:debugging tab + // while it is still initializing. + info("Wait until about:debugging actions are finished"); + await waitForRequestsSuccess(AboutDebugging.store); + + info("Switch back to about:addons"); + await BrowserTestUtils.switchTab(gBrowser, aboutAddonsTab); + is(gBrowser.selectedTab, aboutAddonsTab, "Back to about:addons"); + + info("Re-open about:debugging"); + let switched = TestUtils.waitForCondition( + () => gBrowser.selectedTab == aboutDebuggingTab + ); + debugAddonsBtn.click(); + await switched; + await waitForRequestsToSettle(AboutDebugging.store); + + info("Force about:debugging to a different hash URL"); + aboutDebuggingTab.linkedBrowser.contentWindow.location.hash = "/setup"; + + info("Switch back to about:addons again"); + await BrowserTestUtils.switchTab(gBrowser, aboutAddonsTab); + is(gBrowser.selectedTab, aboutAddonsTab, "Back to about:addons"); + + info("Re-open about:debugging a second time"); + switched = TestUtils.waitForCondition( + () => gBrowser.selectedTab == aboutDebuggingTab + ); + debugAddonsBtn.click(); + await switched; + + info("Wait until any new about:debugging request did settle"); + // Avoid test failures due to closing the about:debugging tab + // while it is still initializing. + await waitForRequestsToSettle(AboutDebugging.store); + + info("Remove the about:debugging tab"); + BrowserTestUtils.removeTab(aboutDebuggingTab); + + await closeView(win); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_addon_list_reordering.js b/toolkit/mozapps/extensions/test/browser/browser_addon_list_reordering.js new file mode 100644 index 0000000000..a80a57bb7e --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_addon_list_reordering.js @@ -0,0 +1,204 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +function assertInSection(card, sectionName, msg) { + let section = card.closest("section"); + let heading = section.querySelector(".list-section-heading"); + is( + card.ownerDocument.l10n.getAttributes(heading).id, + `extension-${sectionName}-heading`, + msg + ); +} + +function waitForAnimationFrame(win) { + return new Promise(resolve => win.requestAnimationFrame(resolve)); +} + +async function clickEnableToggle(card) { + let isDisabled = card.addon.userDisabled; + let addonEvent = isDisabled ? "onEnabled" : "onDisabled"; + let addonStateChanged = AddonTestUtils.promiseAddonEvent(addonEvent); + let win = card.ownerGlobal; + let button = card.querySelector(".extension-enable-button"); + + // Centre the button since "start" could be behind the sticky header. + button.scrollIntoView({ block: "center" }); + EventUtils.synthesizeMouseAtCenter(button, { type: "mousemove" }, win); + EventUtils.synthesizeMouseAtCenter(button, {}, win); + + await addonStateChanged; + await waitForAnimationFrame(win); +} + +function mouseOver(el) { + let win = el.ownerGlobal; + el.scrollIntoView({ block: "center" }); + EventUtils.synthesizeMouseAtCenter(el, { type: "mousemove" }, win); + return waitForAnimationFrame(win); +} + +function mouseOutOfList(win) { + return mouseOver(win.document.querySelector(".header-name")); +} + +function pressKey(win, key) { + EventUtils.synthesizeKey(key, {}, win); + return waitForAnimationFrame(win); +} + +function waitForTransitionEnd(...els) { + return Promise.all( + els.map(el => + BrowserTestUtils.waitForEvent(el, "transitionend", false, e => { + let cardEl = el.firstElementChild; + return e.target == cardEl && e.propertyName == "transform"; + }) + ) + ); +} + +add_setup(async function () { + // Ensure prefers-reduced-motion isn't set. Some linux environments will have + // this enabled by default. + await SpecialPowers.pushPrefEnv({ + set: [["ui.prefersReducedMotion", 0]], + }); +}); + +add_task(async function testReordering() { + let addonIds = [ + "one@mochi.test", + "two@mochi.test", + "three@mochi.test", + "four@mochi.test", + "five@mochi.test", + ]; + let extensions = addonIds.map(id => + ExtensionTestUtils.loadExtension({ + manifest: { + name: id, + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "temporary", + }) + ); + + await Promise.all(extensions.map(ext => ext.startup())); + + let win = await loadInitialView("extension", { withAnimations: true }); + + let cardOne = getAddonCard(win, "one@mochi.test"); + ok(!cardOne.addon.userDisabled, "extension one is enabled"); + assertInSection(cardOne, "enabled", "cardOne is initially in Enabled"); + + await clickEnableToggle(cardOne); + + ok(cardOne.addon.userDisabled, "extension one is now disabled"); + assertInSection(cardOne, "enabled", "cardOne is still in Enabled"); + + let cardThree = getAddonCard(win, "three@mochi.test"); + ok(!cardThree.addon.userDisabled, "extension three is enabled"); + assertInSection(cardThree, "enabled", "cardThree is initially in Enabled"); + + await clickEnableToggle(cardThree); + + ok(cardThree.addon.userDisabled, "extension three is now disabled"); + assertInSection(cardThree, "enabled", "cardThree is still in Enabled"); + + let transitionsEnded = waitForTransitionEnd(cardOne, cardThree); + await mouseOutOfList(win); + await transitionsEnded; + + assertInSection(cardOne, "disabled", "cardOne has moved to disabled"); + assertInSection(cardThree, "disabled", "cardThree has moved to disabled"); + + await clickEnableToggle(cardThree); + await clickEnableToggle(cardOne); + + assertInSection(cardOne, "disabled", "cardOne is still in disabled"); + assertInSection(cardThree, "disabled", "cardThree is still in disabled"); + + info("Opening a more options menu"); + let panel = cardThree.querySelector("panel-list"); + EventUtils.synthesizeMouseAtCenter( + cardThree.querySelector('[action="more-options"]'), + {}, + win + ); + + await BrowserTestUtils.waitForEvent(panel, "shown"); + await mouseOutOfList(win); + + assertInSection(cardOne, "disabled", "cardOne stays in disabled, menu open"); + assertInSection(cardThree, "disabled", "cardThree stays in disabled"); + + transitionsEnded = waitForTransitionEnd(cardOne, cardThree); + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a non-interactive element to clear the focused + // state with a mouse which can be done by assistive technology and keyboard + // by pressing `Esc` key, this rule check shall be ignored by a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + // Click outside the list to clear any focus. + EventUtils.synthesizeMouseAtCenter( + win.document.querySelector(".header-name"), + {}, + win + ); + AccessibilityUtils.resetEnv(); + await transitionsEnded; + + assertInSection(cardOne, "enabled", "cardOne is now in enabled"); + assertInSection(cardThree, "enabled", "cardThree is now in enabled"); + + let cardOneToggle = cardOne.querySelector(".extension-enable-button"); + cardOneToggle.scrollIntoView({ block: "center" }); + cardOneToggle.focus(); + await pressKey(win, " "); + await waitForAnimationFrame(win); + + let cardThreeToggle = cardThree.querySelector(".extension-enable-button"); + let addonList = win.document.querySelector("addon-list"); + // Tab down to cardThreeToggle. + while ( + addonList.contains(win.document.activeElement) && + win.document.activeElement !== cardThreeToggle + ) { + await pressKey(win, "VK_TAB"); + } + await pressKey(win, " "); + + assertInSection(cardOne, "enabled", "cardOne is still in enabled"); + assertInSection(cardThree, "enabled", "cardThree is still in enabled"); + + transitionsEnded = waitForTransitionEnd(cardOne, cardThree); + win.document.querySelector('[action="page-options"]').focus(); + await transitionsEnded; + assertInSection( + cardOne, + "disabled", + "cardOne is now in the disabled section" + ); + assertInSection( + cardThree, + "disabled", + "cardThree is now in the disabled section" + ); + + // Ensure an uninstalled extension is removed right away. + // Hover a card in the middle of the list. + await mouseOver(getAddonCard(win, "two@mochi.test")); + await cardOne.addon.uninstall(true); + ok(!cardOne.parentNode, "cardOne has been removed from the document"); + + await closeView(win); + await Promise.all(extensions.map(ext => ext.unload())); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_amo_abuse_report.js b/toolkit/mozapps/extensions/test/browser/browser_amo_abuse_report.js new file mode 100644 index 0000000000..b470cf2d82 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_amo_abuse_report.js @@ -0,0 +1,85 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* eslint max-len: ["error", 80] */ + +loadTestSubscript("head_abuse_report.js"); + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "extensions.abuseReport.amoFormURL", + "https://example.org/%LOCALE%/%APP%/feedback/addon/%addonID%/", + ], + ], + }); + + // Explicitly flip the amoFormEnabled pref on builds where the pref is + // expected to not be set to true by default. + if (AppConstants.MOZ_APP_NAME != "firefox") { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.abuseReport.amoFormEnabled", true]], + }); + } + + const { AbuseReporter } = ChromeUtils.importESModule( + "resource://gre/modules/AbuseReporter.sys.mjs" + ); + + Assert.equal( + AbuseReporter.amoFormEnabled, + true, + "Expect AMO abuse report form to be enabled" + ); + + // Setting up MockProvider to mock various addon types + // as installed. + await AbuseReportTestUtils.setup(); +}); + +add_task(async function test_opens_amo_form_in_a_tab() { + await openAboutAddons(); + + const ADDON_ID = "test-ext@mochitest"; + const expectedUrl = Services.urlFormatter + .formatURLPref("extensions.abuseReport.amoFormURL") + .replace("%addonID%", ADDON_ID); + + const promiseWaitForAMOFormTab = BrowserTestUtils.waitForNewTab( + gBrowser, + expectedUrl + ); + info("Call about:addons openAbuseReport helper function"); + gManagerWindow.openAbuseReport({ addonId: ADDON_ID }); + info(`Wait for the AMO form url ${expectedUrl} to be opened in a new tab`); + const tab = await promiseWaitForAMOFormTab; + Assert.equal( + tab.linkedBrowser.currentURI.spec, + expectedUrl, + "The newly opened tab has the expected url" + ); + Assert.equal(gBrowser.selectedTab, tab, "The newly opened tab is selected"); + + BrowserTestUtils.removeTab(tab); + await closeAboutAddons(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_report_button_shown_on_dictionary_addons() { + await openAboutAddons("dictionary"); + await AbuseReportTestUtils.assertReportActionShown( + gManagerWindow, + EXT_DICTIONARY_ADDON_ID + ); + await closeAboutAddons(); +}); + +add_task(async function test_report_action_hidden_on_langpack_addons() { + await openAboutAddons("locale"); + await AbuseReportTestUtils.assertReportActionHidden( + gManagerWindow, + EXT_LANGPACK_ADDON_ID + ); + await closeAboutAddons(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug572561.js b/toolkit/mozapps/extensions/test/browser/browser_bug572561.js new file mode 100644 index 0000000000..6f8a56bfba --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_bug572561.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that the locale category is shown if there are no locale packs +// installed but some are pending install + +var gManagerWindow; +var gCategoryUtilities; +var gProvider; +var gInstallProperties = [ + { + name: "Locale Category Test", + type: "locale", + }, +]; +var gInstall; +var gExpectedCancel = false; +var gTestInstallListener = { + onInstallStarted(aInstall) { + check_hidden(false); + }, + + onInstallEnded(aInstall) { + check_hidden(false); + run_next_test(); + }, + + onInstallCancelled(aInstall) { + ok(gExpectedCancel, "Should expect install cancel"); + check_hidden(false); + run_next_test(); + }, + + onInstallFailed(aInstall) { + ok(false, "Did not expect onInstallFailed"); + run_next_test(); + }, +}; + +async function test() { + waitForExplicitFinish(); + + gProvider = new MockProvider(); + + let aWindow = await open_manager("addons://list/extension"); + gManagerWindow = aWindow; + gCategoryUtilities = new CategoryUtilities(gManagerWindow); + run_next_test(); +} + +async function end_test() { + await close_manager(gManagerWindow); + finish(); +} + +function check_hidden(aExpectedHidden) { + var hidden = !gCategoryUtilities.isTypeVisible("locale"); + is(hidden, aExpectedHidden, "Should have correct hidden state"); +} + +// Tests that a non-active install does not make the locale category show +add_test(function () { + check_hidden(true); + gInstall = gProvider.createInstalls(gInstallProperties)[0]; + gInstall.addTestListener(gTestInstallListener); + check_hidden(true); + run_next_test(); +}); + +// Test that restarting the add-on manager with a non-active install +// does not cause the locale category to show +add_test(async function () { + let aWindow = await restart_manager(gManagerWindow, null); + gManagerWindow = aWindow; + gCategoryUtilities = new CategoryUtilities(gManagerWindow); + check_hidden(true); + run_next_test(); +}); + +// Test that installing the install shows the locale category +add_test(function () { + gInstall.install(); +}); + +// Test that restarting the add-on manager does not cause the locale category +// to become hidden +add_test(async function () { + let aWindow = await restart_manager(gManagerWindow, null); + gManagerWindow = aWindow; + gCategoryUtilities = new CategoryUtilities(gManagerWindow); + check_hidden(false); + + gExpectedCancel = true; + gInstall.cancel(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_checkAddonCompatibility.js b/toolkit/mozapps/extensions/test/browser/browser_checkAddonCompatibility.js new file mode 100644 index 0000000000..9cea5b5045 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_checkAddonCompatibility.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that all bundled add-ons are compatible. + +async function test() { + waitForExplicitFinish(); + + Services.prefs.setBoolPref(PREF_STRICT_COMPAT, true); + ok( + AddonManager.strictCompatibility, + "Strict compatibility should be enabled" + ); + + let aAddons = await AddonManager.getAllAddons(); + aAddons.sort(function compareTypeName(a, b) { + return a.type.localeCompare(b.type) || a.name.localeCompare(b.name); + }); + + let allCompatible = true; + for (let a of aAddons) { + // Ignore plugins. + if (a.type == "plugin" || a.id == "workerbootstrap-test@mozilla.org") { + continue; + } + + ok( + a.isCompatible, + a.type + " " + a.name + " " + a.version + " should be compatible" + ); + allCompatible = allCompatible && a.isCompatible; + } + + finish(); +} diff --git a/toolkit/mozapps/extensions/test/browser/browser_colorwaybuiltins_migration.js b/toolkit/mozapps/extensions/test/browser/browser_colorwaybuiltins_migration.js new file mode 100644 index 0000000000..772e327afc --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_colorwaybuiltins_migration.js @@ -0,0 +1,265 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* import-globals-from ../../../../../browser/base/content/test/webextensions/head.js */ +loadTestSubscript( + "../../../../../browser/base/content/test/webextensions/head.js" +); + +const { BuiltInThemes } = ChromeUtils.importESModule( + "resource:///modules/BuiltInThemes.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +const server = AddonTestUtils.createHttpServer(); + +const SERVER_BASE_URL = `http://localhost:${server.identity.primaryPort}`; +const EXPIRED_COLORWAY_THEME_ID1 = "2022red-colorway@mozilla.org"; +const EXPIRED_COLORWAY_THEME_ID2 = "2022orange-colorway@mozilla.org"; +const ICON_SVG = ` + <svg width="63" height="62" viewBox="0 0 63 62" fill="none" xmlns="http://www.w3.org/2000/svg"> + <circle cx="31.5" cy="31" r="31" fill="url(#paint0_linear)"/> + <defs> + <linearGradient id="paint0_linear" x1="44.4829" y1="19" x2="10.4829" y2="53" gradientUnits="userSpaceOnUse"> + <stop stop-color="hsl(147, 94%, 25%)"/> + <stop offset="1" stop-color="hsl(146, 38%, 49%)"/> + </linearGradient> + </defs> + </svg> +`; + +AddonTestUtils.registerJSON(server, "/updates.json", { + addons: { + [EXPIRED_COLORWAY_THEME_ID1]: { + updates: [ + { + version: "2.0.0", + update_link: `${SERVER_BASE_URL}/${EXPIRED_COLORWAY_THEME_ID1}.xpi`, + }, + ], + }, + [EXPIRED_COLORWAY_THEME_ID2]: { + updates: [ + { + version: "3.0.0", + update_link: `${SERVER_BASE_URL}/${EXPIRED_COLORWAY_THEME_ID2}.xpi`, + }, + ], + }, + }, +}); + +const createMockThemeManifest = (id, version) => ({ + name: `Mock theme ${id} ${version}`, + author: "Mozilla", + version, + icons: { 32: "icon.svg" }, + theme: { + colors: { + toolbar: "red", + }, + }, + browser_specific_settings: { + gecko: { id }, + }, +}); + +function createWebExtensionFile(id, version) { + return AddonTestUtils.createTempWebExtensionFile({ + files: { "icon.svg": ICON_SVG }, + manifest: createMockThemeManifest(id, version), + }); +} + +let expiredThemeUpdate1 = createWebExtensionFile( + EXPIRED_COLORWAY_THEME_ID1, + "2.0.0" +); +let expiredThemeUpdate2 = createWebExtensionFile( + EXPIRED_COLORWAY_THEME_ID2, + "3.0.0" +); + +server.registerFile(`/${EXPIRED_COLORWAY_THEME_ID1}.xpi`, expiredThemeUpdate1); +server.registerFile(`/${EXPIRED_COLORWAY_THEME_ID2}.xpi`, expiredThemeUpdate2); + +const goBack = async win => { + let loaded = waitForViewLoad(win); + let backButton = win.document.querySelector(".back-button"); + ok(!backButton.disabled, "back button is enabled"); + backButton.click(); + await loaded; +}; + +const assertAddonCardFound = (win, { addonId, expectColorwayBuiltIn }) => { + const msg = expectColorwayBuiltIn + ? `Found addon card for colorway builtin ${addonId}` + : `Found addon card for migrated colorway ${addonId}`; + + Assert.equal( + getAddonCard(win, addonId)?.addon.isBuiltinColorwayTheme, + expectColorwayBuiltIn, + msg + ); +}; + +const assertDetailView = async (win, { addonId, expectThemeName }) => { + let loadedDetailView = waitForViewLoad(win); + await gBrowser.ownerGlobal.promiseDocumentFlushed(() => {}); + const themeCard = getAddonCard(win, addonId); + // Ensure that we send a click on the control that is accessible (while a + // mouse user could also activate a card by clicking on the entire container): + const themeCardLink = themeCard.querySelector(".addon-name-link"); + themeCardLink.click(); + await loadedDetailView; + Assert.equal( + themeCard.querySelector(".addon-name")?.textContent, + expectThemeName, + `Got the expected addon name in the addon details for ${addonId}` + ); +}; + +async function test_update_expired_colorways_builtins() { + // Set expired theme as a retained colorway theme + const retainedThemePrefName = "browser.theme.retainedExpiredThemes"; + await SpecialPowers.pushPrefEnv({ + set: [ + [PREF_UPDATEURL, `${SERVER_BASE_URL}/updates.json`], + ["extensions.checkUpdateSecurity", false], + ["browser.theme.colorway-migration", true], + [ + retainedThemePrefName, + JSON.stringify([ + EXPIRED_COLORWAY_THEME_ID1, + EXPIRED_COLORWAY_THEME_ID2, + ]), + ], + ], + }); + + await BuiltInThemes.ensureBuiltInThemes(); + async function uninstallTestAddons() { + for (const addonId of [ + EXPIRED_COLORWAY_THEME_ID1, + EXPIRED_COLORWAY_THEME_ID2, + ]) { + info(`Uninstalling test theme ${addonId}`); + let addon = await AddonManager.getAddonByID(addonId); + await addon?.uninstall(); + } + } + registerCleanupFunction(uninstallTestAddons); + + const expiredAddon1 = await AddonManager.getAddonByID( + EXPIRED_COLORWAY_THEME_ID1 + ); + const expiredAddon2 = await AddonManager.getAddonByID( + EXPIRED_COLORWAY_THEME_ID2 + ); + await expiredAddon2.disable(); + await expiredAddon1.enable(); + + info("Open about:addons theme list view"); + let win = await loadInitialView("theme"); + + assertAddonCardFound(win, { + addonId: EXPIRED_COLORWAY_THEME_ID1, + expectColorwayBuiltIn: true, + }); + assertAddonCardFound(win, { + addonId: EXPIRED_COLORWAY_THEME_ID2, + expectColorwayBuiltIn: true, + }); + + info("Trigger addon update check"); + const promiseInstallsEnded = Promise.all([ + AddonTestUtils.promiseInstallEvent( + "onInstallEnded", + install => install.addon.id === EXPIRED_COLORWAY_THEME_ID1 + ), + AddonTestUtils.promiseInstallEvent( + "onInstallEnded", + install => install.addon.id === EXPIRED_COLORWAY_THEME_ID1 + ), + ]); + // Wait for active theme to also execute the update bootstrap method. + let promiseUpdatedAddon1 = waitForUpdate(expiredAddon1); + triggerPageOptionsAction(win, "check-for-updates"); + + info("Wait for addon update to be completed"); + await Promise.all([promiseUpdatedAddon1, promiseInstallsEnded]); + + info("Verify theme list view addon cards"); + assertAddonCardFound(win, { + addonId: EXPIRED_COLORWAY_THEME_ID1, + expectColorwayBuiltIn: false, + }); + assertAddonCardFound(win, { + addonId: EXPIRED_COLORWAY_THEME_ID2, + expectColorwayBuiltIn: false, + }); + + info(`Switch to detail view for theme ${EXPIRED_COLORWAY_THEME_ID1}`); + await assertDetailView(win, { + addonId: EXPIRED_COLORWAY_THEME_ID1, + expectThemeName: `Mock theme ${EXPIRED_COLORWAY_THEME_ID1} 2.0.0`, + }); + + info("Switch back to list view"); + await goBack(win); + assertAddonCardFound(win, { + addonId: EXPIRED_COLORWAY_THEME_ID1, + expectColorwayBuiltIn: false, + }); + assertAddonCardFound(win, { + addonId: EXPIRED_COLORWAY_THEME_ID2, + expectColorwayBuiltIn: false, + }); + + info(`Switch to detail view for theme ${EXPIRED_COLORWAY_THEME_ID2}`); + await assertDetailView(win, { + addonId: EXPIRED_COLORWAY_THEME_ID2, + expectThemeName: `Mock theme ${EXPIRED_COLORWAY_THEME_ID2} 3.0.0`, + }); + + info("Switch back to list view"); + await goBack(win); + assertAddonCardFound(win, { + addonId: EXPIRED_COLORWAY_THEME_ID1, + expectColorwayBuiltIn: false, + }); + assertAddonCardFound(win, { + addonId: EXPIRED_COLORWAY_THEME_ID2, + expectColorwayBuiltIn: false, + }); + + Assert.deepEqual( + JSON.parse( + Services.prefs.getStringPref("browser.theme.retainedExpiredThemes") + ), + [], + "Migrated colorways theme have been removed from the retainedExpiredThemes pref" + ); + + await closeView(win); + await uninstallTestAddons(); + + await SpecialPowers.popPrefEnv(); +} + +add_task(async function test_colorways_builtin_theme_migration() { + await test_update_expired_colorways_builtins(); +}); + +add_task( + async function test_colorways_builtin_theme_migration_on_disabledAutoUpdates() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.update.autoUpdateDefault", false]], + }); + + await test_update_expired_colorways_builtins(); + + await SpecialPowers.popPrefEnv(); + } +); diff --git a/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js b/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js new file mode 100644 index 0000000000..ae8625a18a --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js @@ -0,0 +1,270 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const ABOUT_ADDONS_URL = "chrome://mozapps/content/extensions/aboutaddons.html"; + +const dragService = Cc["@mozilla.org/widget/dragservice;1"].getService( + Ci.nsIDragService +); + +// Test that the drag-drop-addon-installer component installs add-ons and is +// included in about:addons. There is an issue with EventUtils.synthesizeDrop +// where it throws an exception when you give it an subbrowser so we test +// the component directly. + +async function checkInstallConfirmation(...names) { + let notificationCount = 0; + let observer = { + observe(aSubject, aTopic, aData) { + let installInfo = aSubject.wrappedJSObject; + isnot( + installInfo.browser, + null, + "Notification should have non-null browser" + ); + + is( + installInfo.installs.length, + 1, + "Got one AddonInstall instance as expected" + ); + + Assert.deepEqual( + installInfo.installs[0].installTelemetryInfo, + { source: "about:addons", method: "drag-and-drop" }, + "Got the expected installTelemetryInfo" + ); + + notificationCount++; + }, + }; + Services.obs.addObserver(observer, "addon-install-started"); + + let results = []; + + let promise = promisePopupNotificationShown("addon-webext-permissions"); + for (let i = 0; i < names.length; i++) { + let panel = await promise; + let name = panel.getAttribute("name"); + results.push(name); + + info(`Saw install for ${name}`); + if (results.length < names.length) { + info( + `Waiting for installs for ${names.filter(n => !results.includes(n))}` + ); + + promise = promisePopupNotificationShown("addon-webext-permissions"); + } + panel.secondaryButton.click(); + } + + Assert.deepEqual(results.sort(), names.sort(), "Got expected installs"); + + is( + notificationCount, + names.length, + `Saw ${names.length} addon-install-started notification` + ); + Services.obs.removeObserver(observer, "addon-install-started"); +} + +function getDragOverTarget(win) { + return win.document.querySelector("categories-box"); +} + +function getDropTarget(win) { + return win.document.querySelector("drag-drop-addon-installer"); +} + +function withTestPage(fn) { + return BrowserTestUtils.withNewTab( + { url: ABOUT_ADDONS_URL, gBrowser }, + async browser => { + let win = browser.contentWindow; + await win.customElements.whenDefined("drag-drop-addon-installer"); + await fn(browser); + } + ); +} + +function initDragSession({ dragData, dropEffect }) { + let dropAction; + switch (dropEffect) { + case null: + case undefined: + case "move": + dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_MOVE; + break; + case "copy": + dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_COPY; + break; + case "link": + dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_LINK; + break; + default: + throw new Error(`${dropEffect} is an invalid drop effect value`); + } + + const dataTransfer = new DataTransfer(); + dataTransfer.dropEffect = dropEffect; + + for (let i = 0; i < dragData.length; i++) { + const item = dragData[i]; + for (let j = 0; j < item.length; j++) { + dataTransfer.mozSetDataAt(item[j].type, item[j].data, i); + } + } + + dragService.startDragSessionForTests(dropAction); + const session = dragService.getCurrentSession(); + session.dataTransfer = dataTransfer; + + return session; +} + +async function simulateDragAndDrop(win, dragData) { + const dropTarget = getDropTarget(win); + const dragOverTarget = getDragOverTarget(win); + const dropEffect = "move"; + + const session = initDragSession({ dragData, dropEffect }); + + info("Simulate drag over and wait for the drop target to be visible"); + + EventUtils.synthesizeDragOver( + dragOverTarget, + dragOverTarget, + dragData, + dropEffect, + win + ); + + // This make sure that the fake dataTransfer has still + // the expected drop effect after the synthesizeDragOver call. + session.dataTransfer.dropEffect = "move"; + + await BrowserTestUtils.waitForCondition( + () => !dropTarget.hidden, + "Wait for the drop target element to be visible" + ); + + info("Simulate drop dragData on drop target"); + + EventUtils.synthesizeDropAfterDragOver( + null, + session.dataTransfer, + dropTarget, + win, + { _domDispatchOnly: true } + ); + + dragService.endDragSession(true); +} + +// Simulates dropping a URL onto the manager +add_task(async function test_drop_url() { + for (let fileType of ["xpi", "zip"]) { + await withTestPage(async browser => { + const url = TESTROOT + `addons/browser_dragdrop1.${fileType}`; + const promise = checkInstallConfirmation("Drag Drop test 1"); + + await simulateDragAndDrop(browser.contentWindow, [ + [{ type: "text/x-moz-url", data: url }], + ]); + + await promise; + }); + } +}); + +// Simulates dropping a file onto the manager +add_task(async function test_drop_file() { + for (let fileType of ["xpi", "zip"]) { + await withTestPage(async browser => { + let fileurl = get_addon_file_url(`browser_dragdrop1.${fileType}`); + let promise = checkInstallConfirmation("Drag Drop test 1"); + + await simulateDragAndDrop(browser.contentWindow, [ + [{ type: "application/x-moz-file", data: fileurl.file }], + ]); + + await promise; + }); + } +}); + +// Simulates dropping two urls onto the manager +add_task(async function test_drop_multiple_urls() { + await withTestPage(async browser => { + let url1 = TESTROOT + "addons/browser_dragdrop1.xpi"; + let url2 = TESTROOT2 + "addons/browser_dragdrop2.zip"; + let promise = checkInstallConfirmation( + "Drag Drop test 1", + "Drag Drop test 2" + ); + + await simulateDragAndDrop(browser.contentWindow, [ + [{ type: "text/x-moz-url", data: url1 }], + [{ type: "text/x-moz-url", data: url2 }], + ]); + + await promise; + }); +}).skip(); // TODO(rpl): this fails because mozSetDataAt throws IndexSizeError. + +// Simulates dropping two files onto the manager +add_task(async function test_drop_multiple_files() { + await withTestPage(async browser => { + let fileurl1 = get_addon_file_url("browser_dragdrop1.zip"); + let fileurl2 = get_addon_file_url("browser_dragdrop2.xpi"); + let promise = checkInstallConfirmation( + "Drag Drop test 1", + "Drag Drop test 2" + ); + + await simulateDragAndDrop(browser.contentWindow, [ + [{ type: "application/x-moz-file", data: fileurl1.file }], + [{ type: "application/x-moz-file", data: fileurl2.file }], + ]); + + await promise; + }); +}).skip(); // TODO(rpl): this fails because mozSetDataAt throws IndexSizeError. + +// Simulates dropping a file and a url onto the manager (weird, but should still work) +add_task(async function test_drop_file_and_url() { + await withTestPage(async browser => { + let url = TESTROOT + "addons/browser_dragdrop1.xpi"; + let fileurl = get_addon_file_url("browser_dragdrop2.zip"); + let promise = checkInstallConfirmation( + "Drag Drop test 1", + "Drag Drop test 2" + ); + + await simulateDragAndDrop(browser.contentWindow, [ + [{ type: "text/x-moz-url", data: url }], + [{ type: "application/x-moz-file", data: fileurl.file }], + ]); + + await promise; + }); +}).skip(); // TODO(rpl): this fails because mozSetDataAt throws IndexSizeError. + +// Test that drag-and-drop of an incompatible addon generates +// an error. +add_task(async function test_drop_incompat_file() { + await withTestPage(async browser => { + let url = `${TESTROOT}/addons/browser_dragdrop_incompat.xpi`; + + let panelPromise = promisePopupNotificationShown("addon-install-failed"); + await simulateDragAndDrop(browser.contentWindow, [ + [{ type: "text/x-moz-url", data: url }], + ]); + + let panel = await panelPromise; + ok(panel, "Got addon-install-failed popup"); + panel.button.click(); + }); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_file_xpi_no_process_switch.js b/toolkit/mozapps/extensions/test/browser/browser_file_xpi_no_process_switch.js new file mode 100644 index 0000000000..6793363698 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_file_xpi_no_process_switch.js @@ -0,0 +1,122 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ + +const ADDON_INSTALL_ID = "addon-webext-permissions"; + +let fileurl1 = get_addon_file_url("browser_dragdrop1.xpi"); +let fileurl2 = get_addon_file_url("browser_dragdrop2.xpi"); + +function promiseInstallNotification(aBrowser) { + return new Promise(resolve => { + function popupshown(event) { + let notification = PopupNotifications.getNotification( + ADDON_INSTALL_ID, + aBrowser + ); + if (!notification) { + return; + } + + if (gBrowser.selectedBrowser !== aBrowser) { + return; + } + + PopupNotifications.panel.removeEventListener("popupshown", popupshown); + ok(true, `Got ${ADDON_INSTALL_ID} popup for browser`); + event.target.firstChild.secondaryButton.click(); + resolve(); + } + + PopupNotifications.panel.addEventListener("popupshown", popupshown); + }); +} + +function CheckBrowserInPid(browser, expectedPid, message) { + return SpecialPowers.spawn(browser, [{ expectedPid, message }], arg => { + is(Services.appinfo.processID, arg.expectedPid, arg.message); + }); +} + +async function testOpenedAndDraggedXPI(aBrowser) { + // Get the current pid for browser for comparison later. + let browserPid = await SpecialPowers.spawn(aBrowser, [], () => { + return Services.appinfo.processID; + }); + + // No process switch for XPI file:// URI in the urlbar. + let promiseNotification = promiseInstallNotification(aBrowser); + let urlbar = gURLBar; + urlbar.value = fileurl1.spec; + urlbar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + await promiseNotification; + await CheckBrowserInPid( + aBrowser, + browserPid, + "Check that browser has not switched process." + ); + + // No process switch for XPI file:// URI dragged to tab. + let tab = gBrowser.getTabForBrowser(aBrowser); + promiseNotification = promiseInstallNotification(aBrowser); + let effect = EventUtils.synthesizeDrop( + tab, + tab, + [[{ type: "text/uri-list", data: fileurl1.spec }]], + "move" + ); + is(effect, "move", "Drag should be accepted"); + await promiseNotification; + await CheckBrowserInPid( + aBrowser, + browserPid, + "Check that browser has not switched process." + ); + + // No process switch for two XPI file:// URIs dragged to tab. + promiseNotification = promiseInstallNotification(aBrowser); + let promiseNewTab = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + effect = EventUtils.synthesizeDrop( + tab, + tab, + [ + [{ type: "text/uri-list", data: fileurl1.spec }], + [{ type: "text/uri-list", data: fileurl2.spec }], + ], + "move" + ); + is(effect, "move", "Drag should be accepted"); + // When drag'n'dropping two XPIs, one is loaded in the current tab while the + // other one is loaded in a new tab. + let { target: newTab } = await promiseNewTab; + // This is the prompt for the first XPI in the current tab. + await promiseNotification; + + let promiseSecondNotification = promiseInstallNotification( + newTab.linkedBrowser + ); + + // We switch to the second tab and wait for the prompt for the second XPI. + BrowserTestUtils.switchTab(gBrowser, newTab); + await promiseSecondNotification; + + BrowserTestUtils.removeTab(newTab); + + await CheckBrowserInPid( + aBrowser, + browserPid, + "Check that browser has not switched process." + ); +} + +// Test for bug 1175267. +add_task(async function () { + await BrowserTestUtils.withNewTab( + "http://example.com", + testOpenedAndDraggedXPI + ); + await BrowserTestUtils.withNewTab("about:robots", testOpenedAndDraggedXPI); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_globalwarnings.js b/toolkit/mozapps/extensions/test/browser/browser_globalwarnings.js new file mode 100644 index 0000000000..368160698f --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_globalwarnings.js @@ -0,0 +1,176 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Bug 566194 - safe mode / security & compatibility check status are not exposed in new addon manager UI + +async function loadDetail(win, id) { + let loaded = waitForViewLoad(win); + // Check the detail view. + let card = win.document.querySelector(`addon-card[addon-id="${id}"]`); + EventUtils.synthesizeMouseAtCenter( + card.querySelector(".addon-name-link"), + {}, + win + ); + await loaded; +} + +function checkMessageShown(win, type, hasButton) { + let stack = win.document.querySelector("global-warnings"); + is(stack.childElementCount, 1, "There is one message"); + let messageBar = stack.firstElementChild; + ok(messageBar, "There is a message bar"); + is( + messageBar.localName, + "moz-message-bar", + "The message bar is a moz-message-bar" + ); + is_element_visible(messageBar, "Message bar is visible"); + is(messageBar.getAttribute("warning-type"), type); + if (hasButton) { + let button = messageBar.querySelector("button"); + is_element_visible(button, "Button is visible"); + is(button.getAttribute("action"), type, "Button action is set"); + } +} + +function checkNoMessages(win) { + let stack = win.document.querySelector("global-warnings"); + if (stack.childElementCount) { + // The safe mode message is hidden in CSS on the plugin list. + for (let child of stack.children) { + is_element_hidden(child, "The message is hidden"); + } + } else { + is(stack.childElementCount, 0, "There are no message bars"); + } +} + +function clickMessageAction(win) { + let stack = win.document.querySelector("global-warnings"); + let button = stack.firstElementChild.querySelector("button"); + EventUtils.synthesizeMouseAtCenter(button, {}, win); +} + +add_task(async function checkCompatibility() { + info("Testing compatibility checking warning"); + + info("Setting checkCompatibility to false"); + AddonManager.checkCompatibility = false; + + let id = "test@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { browser_specific_settings: { gecko: { id } } }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + + // Check the extension list view. + checkMessageShown(win, "check-compatibility", true); + + // Check the detail view. + await loadDetail(win, id); + checkMessageShown(win, "check-compatibility", true); + + // Check other views. + let views = ["plugin", "theme"]; + for (let view of views) { + await switchView(win, view); + checkMessageShown(win, "check-compatibility", true); + } + + // Check the button works. + info("Clicking 'Enable' button"); + clickMessageAction(win); + is( + AddonManager.checkCompatibility, + true, + "Check Compatibility pref should be cleared" + ); + checkNoMessages(win); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function checkSecurity() { + info("Testing update security checking warning"); + + var pref = "extensions.checkUpdateSecurity"; + info("Setting " + pref + " pref to false"); + await SpecialPowers.pushPrefEnv({ + set: [[pref, false]], + }); + + let id = "test-security@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { browser_specific_settings: { gecko: { id } } }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + + // Check extension list view. + checkMessageShown(win, "update-security", true); + + // Check detail view. + await loadDetail(win, id); + checkMessageShown(win, "update-security", true); + + // Check other views. + let views = ["plugin", "theme"]; + for (let view of views) { + await switchView(win, view); + checkMessageShown(win, "update-security", true); + } + + // Check the button works. + info("Clicking 'Enable' button"); + clickMessageAction(win); + is( + Services.prefs.prefHasUserValue(pref), + false, + "Check Update Security pref should be cleared" + ); + checkNoMessages(win); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function checkSafeMode() { + info("Testing safe mode warning"); + + let id = "test-safemode@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { browser_specific_settings: { gecko: { id } } }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + + // Check extension list view hidden. + checkNoMessages(win); + + let globalWarnings = win.document.querySelector("global-warnings"); + globalWarnings.inSafeMode = true; + globalWarnings.refresh(); + + // Check detail view. + await loadDetail(win, id); + checkMessageShown(win, "safe-mode"); + + // Check other views. + await switchView(win, "theme"); + checkMessageShown(win, "safe-mode"); + await switchView(win, "plugin"); + checkNoMessages(win); + + await closeView(win); + await extension.unload(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js b/toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js new file mode 100644 index 0000000000..51ffbc6cdd --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js @@ -0,0 +1,406 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { GMPInstallManager } = ChromeUtils.importESModule( + "resource://gre/modules/GMPInstallManager.sys.mjs" +); +const { GMPPrefs, GMP_PLUGIN_IDS, WIDEVINE_L1_ID, WIDEVINE_L3_ID } = + ChromeUtils.importESModule("resource://gre/modules/GMPUtils.sys.mjs"); + +const TEST_DATE = new Date(2013, 0, 1, 12); + +var gMockAddons = []; + +for (let pluginId of GMP_PLUGIN_IDS) { + let mockAddon = Object.freeze({ + id: pluginId, + isValid: true, + isInstalled: false, + isEME: pluginId == WIDEVINE_L1_ID || pluginId == WIDEVINE_L3_ID, + usedFallback: true, + }); + gMockAddons.push(mockAddon); +} + +var gInstalledAddonId = ""; +var gInstallDeferred = null; +var gPrefs = Services.prefs; +var getKey = GMPPrefs.getPrefKey; + +const MockGMPInstallManagerPrototype = { + checkForAddons: () => + Promise.resolve({ + addons: gMockAddons, + }), + + installAddon: addon => { + gInstalledAddonId = addon.id; + gInstallDeferred.resolve(); + return Promise.resolve(); + }, +}; + +function openDetailsView(win, id) { + let item = getAddonCard(win, id); + Assert.ok(item, "Should have got add-on element."); + is_element_visible(item, "Add-on element should be visible."); + + let loaded = waitForViewLoad(win); + EventUtils.synthesizeMouseAtCenter( + item.querySelector(".addon-name-link"), + {}, + item.ownerGlobal + ); + return loaded; +} + +add_task(async function initializeState() { + gPrefs.setBoolPref(GMPPrefs.KEY_LOGGING_DUMP, true); + gPrefs.setIntPref(GMPPrefs.KEY_LOGGING_LEVEL, 0); + + registerCleanupFunction(async function () { + for (let addon of gMockAddons) { + gPrefs.clearUserPref(getKey(GMPPrefs.KEY_PLUGIN_ENABLED, addon.id)); + gPrefs.clearUserPref(getKey(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, addon.id)); + gPrefs.clearUserPref(getKey(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id)); + gPrefs.clearUserPref(getKey(GMPPrefs.KEY_PLUGIN_VERSION, addon.id)); + gPrefs.clearUserPref(getKey(GMPPrefs.KEY_PLUGIN_VISIBLE, addon.id)); + gPrefs.clearUserPref( + getKey(GMPPrefs.KEY_PLUGIN_FORCE_SUPPORTED, addon.id) + ); + } + gPrefs.clearUserPref(GMPPrefs.KEY_LOGGING_DUMP); + gPrefs.clearUserPref(GMPPrefs.KEY_LOGGING_LEVEL); + gPrefs.clearUserPref(GMPPrefs.KEY_UPDATE_LAST_CHECK); + gPrefs.clearUserPref(GMPPrefs.KEY_EME_ENABLED); + }); + + // Start out with plugins not being installed, disabled and automatic updates + // disabled. + gPrefs.setBoolPref(GMPPrefs.KEY_EME_ENABLED, true); + for (let addon of gMockAddons) { + gPrefs.setBoolPref(getKey(GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), false); + gPrefs.setIntPref(getKey(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, addon.id), 0); + gPrefs.setBoolPref(getKey(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id), false); + gPrefs.setCharPref(getKey(GMPPrefs.KEY_PLUGIN_VERSION, addon.id), ""); + gPrefs.setBoolPref(getKey(GMPPrefs.KEY_PLUGIN_VISIBLE, addon.id), true); + gPrefs.setBoolPref( + getKey(GMPPrefs.KEY_PLUGIN_FORCE_SUPPORTED, addon.id), + true + ); + } +}); + +add_task(async function testNotInstalledDisabled() { + let win = await loadInitialView("extension"); + + Assert.ok(isCategoryVisible(win, "plugin"), "Plugin tab visible."); + await switchView(win, "plugin"); + + for (let addon of gMockAddons) { + let addonCard = getAddonCard(win, addon.id); + Assert.ok(addonCard, "Got add-on element:" + addon.id); + + is( + addonCard.ownerDocument.l10n.getAttributes(addonCard.addonNameEl).id, + "addon-name-disabled", + "The addon name should include a disabled postfix" + ); + + let cardMessage = addonCard.querySelector( + "moz-message-bar.addon-card-message" + ); + is_element_hidden(cardMessage, "Warning notification is hidden"); + } + + await closeView(win); +}); + +add_task(async function testNotInstalledDisabledDetails() { + let win = await loadInitialView("plugin"); + + for (let addon of gMockAddons) { + await openDetailsView(win, addon.id); + let addonCard = getAddonCard(win, addon.id); + ok(addonCard, "Got add-on element: " + addon.id); + + is( + win.document.l10n.getAttributes(addonCard.addonNameEl).id, + "addon-name-disabled", + "The addon name should include a disabled postfix" + ); + + let updatesBtn = addonCard.querySelector("[action=update-check]"); + is_element_visible(updatesBtn, "Check for Updates action is visible"); + let cardMessage = addonCard.querySelector( + "moz-message-bar.addon-card-message" + ); + is_element_hidden(cardMessage, "Warning notification is hidden"); + + await switchView(win, "plugin"); + } + + await closeView(win); +}); + +add_task(async function testNotInstalled() { + let win = await loadInitialView("plugin"); + + for (let addon of gMockAddons) { + gPrefs.setBoolPref(getKey(GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), true); + let item = getAddonCard(win, addon.id); + Assert.ok(item, "Got add-on element:" + addon.id); + + let warningMessageBar = await BrowserTestUtils.waitForCondition(() => { + return item.querySelector( + "moz-message-bar.addon-card-message[type=warning]" + ); + }, "Wait for the addon card message to be updated"); + + is_element_visible(warningMessageBar, "Warning notification is visible"); + + is(item.parentNode.getAttribute("section"), "0", "Should be enabled"); + // Open the options menu (needed to check the disabled buttons). + const pluginOptions = item.querySelector("plugin-options"); + pluginOptions.querySelector("panel-list").open = true; + const alwaysActivate = pluginOptions.querySelector( + "panel-item[action=always-activate]" + ); + ok( + alwaysActivate.hasAttribute("checked"), + "Plugin state should be always-activate" + ); + pluginOptions.querySelector("panel-list").open = false; + } + + await closeView(win); +}); + +add_task(async function testNotInstalledDetails() { + let win = await loadInitialView("plugin"); + + for (let addon of gMockAddons) { + await openDetailsView(win, addon.id); + + const addonCard = getAddonCard(win, addon.id); + let el = addonCard.querySelector("[action=update-check]"); + is_element_visible(el, "Check for Updates action is visible"); + + let warningMessageBar = await BrowserTestUtils.waitForCondition(() => { + return addonCard.querySelector( + "moz-message-bar.addon-card-message[type=warning]" + ); + }, "Wait for the addon card message to be updated"); + is_element_visible(warningMessageBar, "Warning notification is visible"); + + await switchView(win, "plugin"); + } + + await closeView(win); +}); + +add_task(async function testInstalled() { + let win = await loadInitialView("plugin"); + + for (let addon of gMockAddons) { + gPrefs.setIntPref( + getKey(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, addon.id), + TEST_DATE.getTime() + ); + gPrefs.setBoolPref(getKey(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id), false); + gPrefs.setCharPref( + getKey(GMPPrefs.KEY_PLUGIN_VERSION, addon.id), + "1.2.3.4" + ); + + let item = getAddonCard(win, addon.id); + Assert.ok(item, "Got add-on element."); + + is(item.parentNode.getAttribute("section"), "0", "Should be enabled"); + // Open the options menu (needed to check the disabled buttons). + const pluginOptions = item.querySelector("plugin-options"); + pluginOptions.querySelector("panel-list").open = true; + const alwaysActivate = pluginOptions.querySelector( + "panel-item[action=always-activate]" + ); + ok( + alwaysActivate.hasAttribute("checked"), + "Plugin state should be always-activate" + ); + pluginOptions.querySelector("panel-list").open = false; + } + + await closeView(win); +}); + +add_task(async function testInstalledDetails() { + let win = await loadInitialView("plugin"); + + for (let addon of gMockAddons) { + await openDetailsView(win, addon.id); + + let card = getAddonCard(win, addon.id); + ok(card, "Got add-on element:" + addon.id); + + is_element_visible( + card.querySelector("[action=update-check]"), + "Find updates link is visible" + ); + + await switchView(win, "plugin"); + } + + await closeView(win); +}); + +add_task(async function testInstalledGlobalEmeDisabled() { + let win = await loadInitialView("plugin"); + gPrefs.setBoolPref(GMPPrefs.KEY_EME_ENABLED, false); + + for (let addon of gMockAddons) { + let item = getAddonCard(win, addon.id); + if (addon.isEME) { + is(item.parentNode.getAttribute("section"), "1", "Should be disabled"); + } else { + Assert.ok(item, "Got add-on element."); + } + } + + gPrefs.setBoolPref(GMPPrefs.KEY_EME_ENABLED, true); + await closeView(win); +}); + +add_task(async function testPreferencesButton() { + let prefValues = [ + { enabled: false, version: "" }, + { enabled: false, version: "1.2.3.4" }, + { enabled: true, version: "" }, + { enabled: true, version: "1.2.3.4" }, + ]; + + for (let preferences of prefValues) { + info( + "Testing preferences button with pref settings: " + + JSON.stringify(preferences) + ); + for (let addon of gMockAddons) { + let win = await loadInitialView("plugin"); + gPrefs.setCharPref( + getKey(GMPPrefs.KEY_PLUGIN_VERSION, addon.id), + preferences.version + ); + gPrefs.setBoolPref( + getKey(GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), + preferences.enabled + ); + + let item = getAddonCard(win, addon.id); + + // Open the options menu (needed to check the more options action is enabled). + const pluginOptions = item.querySelector("plugin-options"); + pluginOptions.querySelector("panel-list").open = true; + const moreOptions = pluginOptions.querySelector( + "panel-item[action=expand]" + ); + ok( + !moreOptions.shadowRoot.querySelector("button").disabled, + "more options action should be enabled" + ); + moreOptions.click(); + + await waitForViewLoad(win); + + item = getAddonCard(win, addon.id); + ok(item, "The right view is loaded"); + + await closeView(win); + } + } +}); + +add_task(async function testUpdateButton() { + gPrefs.clearUserPref(GMPPrefs.KEY_UPDATE_LAST_CHECK); + + // The GMPInstallManager constructor has an empty body, + // so replacing the prototype is safe. + let originalInstallManager = GMPInstallManager.prototype; + GMPInstallManager.prototype = MockGMPInstallManagerPrototype; + + let win = await loadInitialView("plugin"); + + for (let addon of gMockAddons) { + let item = getAddonCard(win, addon.id); + + gInstalledAddonId = ""; + gInstallDeferred = Promise.withResolvers(); + + let loaded = waitForViewLoad(win); + item.querySelector("[action=expand]").click(); + await loaded; + let detail = getAddonCard(win, addon.id); + detail.querySelector("[action=update-check]").click(); + + await gInstallDeferred.promise; + Assert.equal(gInstalledAddonId, addon.id); + + await switchView(win, "plugin"); + } + + GMPInstallManager.prototype = originalInstallManager; + + await closeView(win); +}); + +add_task(async function testEmeSupport() { + for (let addon of gMockAddons) { + gPrefs.clearUserPref(getKey(GMPPrefs.KEY_PLUGIN_FORCE_SUPPORTED, addon.id)); + } + + let win = await loadInitialView("plugin"); + + for (let addon of gMockAddons) { + let item = getAddonCard(win, addon.id); + if (addon.id == WIDEVINE_L1_ID) { + if ( + AppConstants.MOZ_WMF_CDM && + AppConstants.platform == "win" && + UpdateUtils.ABI.match(/x64/) + ) { + Assert.ok(item, "Widevine L1 supported, found add-on element."); + } else { + Assert.ok( + !item, + "Widevine L1 not supported, couldn't find add-on element." + ); + } + } else if (addon.id == WIDEVINE_L3_ID) { + if ( + AppConstants.platform == "win" || + AppConstants.platform == "macosx" || + AppConstants.platform == "linux" + ) { + Assert.ok(item, "Widevine L3 supported, found add-on element."); + } else { + Assert.ok( + !item, + "Widevine L3 not supported, couldn't find add-on element." + ); + } + } else { + Assert.ok(item, "Found add-on element."); + } + } + + await closeView(win); + + for (let addon of gMockAddons) { + gPrefs.setBoolPref(getKey(GMPPrefs.KEY_PLUGIN_VISIBLE, addon.id), true); + gPrefs.setBoolPref( + getKey(GMPPrefs.KEY_PLUGIN_FORCE_SUPPORTED, addon.id), + true + ); + } +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_history_navigation.js b/toolkit/mozapps/extensions/test/browser/browser_history_navigation.js new file mode 100644 index 0000000000..2b177bc7cd --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_history_navigation.js @@ -0,0 +1,623 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* eslint max-nested-callbacks: ["warn", 12] */ + +/** + * Tests that history navigation works for the add-ons manager. + */ + +// Request a longer timeout, because this tests run twice +// (once on XUL views and once on the HTML views). +requestLongerTimeout(4); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +const DISCOAPI_URL = `http://example.com/${RELATIVE_DIR}/discovery/api_response_empty.json`; + +SpecialPowers.pushPrefEnv({ + set: [["browser.navigation.requireUserInteraction", false]], +}); + +var gProvider = new MockProvider(); +gProvider.createAddons([ + { + id: "test1@tests.mozilla.org", + name: "Test add-on 1", + description: "foo", + }, + { + id: "test2@tests.mozilla.org", + name: "Test add-on 2", + description: "bar", + }, + { + id: "test3@tests.mozilla.org", + name: "Test add-on 3", + type: "theme", + description: "bar", + }, +]); + +function go_back() { + gBrowser.goBack(); +} + +const goBackKeyModifier = + AppConstants.platform == "macosx" ? { metaKey: true } : { altKey: true }; + +function go_back_key() { + EventUtils.synthesizeKey("KEY_ArrowLeft", goBackKeyModifier); +} + +function go_forward_key() { + EventUtils.synthesizeKey("KEY_ArrowRight", goBackKeyModifier); +} + +function go_forward() { + gBrowser.goForward(); +} + +function check_state(canGoBack, canGoForward) { + is(gBrowser.canGoBack, canGoBack, "canGoBack should be correct"); + is(gBrowser.canGoForward, canGoForward, "canGoForward should be correct"); +} + +function is_in_list(aManager, view, canGoBack, canGoForward) { + var categoryUtils = new CategoryUtilities(aManager); + + is( + categoryUtils.getSelectedViewId(), + view, + "Should be on the right category" + ); + + ok( + aManager.document.querySelector("addon-list"), + "Got a list-view in about:addons" + ); + + check_state(canGoBack, canGoForward); +} + +function is_in_detail(aManager, view, canGoBack, canGoForward) { + var categoryUtils = new CategoryUtilities(aManager); + + is( + categoryUtils.getSelectedViewId(), + view, + "Should be on the right category" + ); + + is( + aManager.document.querySelectorAll("addon-card").length, + 1, + "Got a detail-view in about:addons" + ); + + check_state(canGoBack, canGoForward); +} + +function is_in_discovery(aManager, canGoBack, canGoForward) { + ok( + aManager.document.querySelector("discovery-pane"), + "Got a discovery panel in the HTML about:addons browser" + ); + + check_state(canGoBack, canGoForward); +} + +async function expand_addon_element(aManagerWin, aId) { + var addon = getAddonCard(aManagerWin, aId); + // Ensure that we send a click on the control that is accessible (while a + // mouse user could also activate a card by clicking on the entire container): + const addonLink = addon.querySelector(".addon-name-link"); + addonLink.click(); +} + +function wait_for_page_load(browser) { + return BrowserTestUtils.browserLoaded(browser, false, "http://example.com/"); +} + +// Tests simple forward and back navigation and that the right heading and +// category is selected +add_task(async function test_navigate_history() { + let aManager = await open_manager("addons://list/extension"); + let categoryUtils = new CategoryUtilities(aManager); + info("Part 1"); + is_in_list(aManager, "addons://list/extension", false, false); + + EventUtils.synthesizeMouseAtCenter(categoryUtils.get("plugin"), {}, aManager); + + aManager = await wait_for_view_load(aManager); + info("Part 2"); + is_in_list(aManager, "addons://list/plugin", true, false); + + go_back(); + + aManager = await wait_for_view_load(aManager); + info("Part 3"); + is_in_list(aManager, "addons://list/extension", false, true); + + go_forward(); + + aManager = await wait_for_view_load(aManager); + info("Part 4"); + is_in_list(aManager, "addons://list/plugin", true, false); + + go_back(); + + aManager = await wait_for_view_load(aManager); + info("Part 5"); + is_in_list(aManager, "addons://list/extension", false, true); + + await expand_addon_element(aManager, "test1@tests.mozilla.org"); + + aManager = await wait_for_view_load(aManager); + info("Part 6"); + is_in_detail(aManager, "addons://list/extension", true, false); + + go_back(); + + aManager = await wait_for_view_load(aManager); + info("Part 7"); + is_in_list(aManager, "addons://list/extension", false, true); + + await close_manager(aManager); +}); + +// Tests that browsing to the add-ons manager from a website and going back works +add_task(async function test_navigate_between_webpage_and_aboutaddons() { + info("Part 1"); + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com/", + true, + true + ); + + info("Part 2"); + ok(!gBrowser.canGoBack, "Should not be able to go back"); + ok(!gBrowser.canGoForward, "Should not be able to go forward"); + + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + "about:addons" + ); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + let manager = await wait_for_manager_load( + gBrowser.selectedBrowser.contentWindow + ); + + info("Part 3"); + is_in_list(manager, "addons://list/extension", true, false); + + // XXX: This is less than ideal, as it's currently difficult to deal with + // the browser frame switching between remote/non-remote in e10s mode. + let promiseLoaded; + if (gMultiProcessBrowser) { + promiseLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + } else { + promiseLoaded = BrowserTestUtils.waitForEvent( + gBrowser.selectedBrowser, + "pageshow" + ); + } + + go_back(manager); + await promiseLoaded; + + info("Part 4"); + is( + gBrowser.currentURI.spec, + "http://example.com/", + "Should be showing the webpage" + ); + ok(!gBrowser.canGoBack, "Should not be able to go back"); + ok(gBrowser.canGoForward, "Should be able to go forward"); + + promiseLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + go_forward(manager); + await promiseLoaded; + + manager = gBrowser.selectedBrowser.contentWindow; + info("Part 5"); + await TestUtils.waitForCondition( + () => manager.document.querySelector("addon-list"), + "The add-on list should render." + ); + + is_in_list(manager, "addons://list/extension", true, false); + + await close_manager(manager); +}); + +// Tests simple forward and back navigation and that the right heading and +// category is selected -- Keyboard navigation [Bug 565359] +add_task(async function test_keyboard_history_navigation() { + let aManager = await open_manager("addons://list/extension"); + let categoryUtils = new CategoryUtilities(aManager); + info("Part 1"); + is_in_list(aManager, "addons://list/extension", false, false); + + EventUtils.synthesizeMouseAtCenter(categoryUtils.get("plugin"), {}, aManager); + + aManager = await wait_for_view_load(aManager); + info("Part 2"); + is_in_list(aManager, "addons://list/plugin", true, false); + + // Backspace should not navigate back. We should still be on the same view. + is( + Services.prefs.getIntPref("browser.backspace_action"), + 2, + "Backspace should not navigate back" + ); + EventUtils.synthesizeKey("KEY_Backspace"); + aManager = await wait_for_view_load(aManager); + info("Part 2b"); + is_in_list(aManager, "addons://list/plugin", true, false); + + go_back_key(); + + aManager = await wait_for_view_load(aManager); + info("Part 3"); + is_in_list(aManager, "addons://list/extension", false, true); + + go_forward_key(); + + aManager = await wait_for_view_load(aManager); + info("Part 4"); + is_in_list(aManager, "addons://list/plugin", true, false); + + go_back_key(); + + aManager = await wait_for_view_load(aManager); + info("Part 5"); + is_in_list(aManager, "addons://list/extension", false, true); + + await expand_addon_element(aManager, "test1@tests.mozilla.org"); + + aManager = await wait_for_view_load(aManager); + info("Part 6"); + is_in_detail(aManager, "addons://list/extension", true, false); + + go_back_key(); + + aManager = await wait_for_view_load(aManager); + info("Part 7"); + is_in_list(aManager, "addons://list/extension", false, true); + + await close_manager(aManager); +}); + +// Tests that opening a custom first view only stores a single history entry +add_task(async function test_single_history_entry() { + let aManager = await open_manager("addons://list/plugin"); + let categoryUtils = new CategoryUtilities(aManager); + info("Part 1"); + is_in_list(aManager, "addons://list/plugin", false, false); + + EventUtils.synthesizeMouseAtCenter( + categoryUtils.get("extension"), + {}, + aManager + ); + + aManager = await wait_for_view_load(aManager); + info("Part 2"); + is_in_list(aManager, "addons://list/extension", true, false); + + go_back(); + + aManager = await wait_for_view_load(aManager); + info("Part 3"); + is_in_list(aManager, "addons://list/plugin", false, true); + + await close_manager(aManager); +}); + +// Tests that opening a view while the manager is already open adds a new +// history entry +add_task(async function test_new_history_entry_while_opened() { + let aManager = await open_manager("addons://list/extension"); + info("Part 1"); + is_in_list(aManager, "addons://list/extension", false, false); + + aManager.loadView("addons://list/plugin"); + + aManager = await wait_for_view_load(aManager); + info("Part 2"); + is_in_list(aManager, "addons://list/plugin", true, false); + + go_back(); + + aManager = await wait_for_view_load(aManager); + info("Part 3"); + is_in_list(aManager, "addons://list/extension", false, true); + + go_forward(); + + aManager = await wait_for_view_load(aManager); + info("Part 4"); + is_in_list(aManager, "addons://list/plugin", true, false); + + await close_manager(aManager); +}); + +// Tests than navigating to a website and then going back returns to the +// previous view +add_task(async function test_navigate_back_from_website() { + await SpecialPowers.pushPrefEnv({ + set: [["security.allow_eval_with_system_principal", true]], + }); + + let aManager = await open_manager("addons://list/plugin"); + info("Part 1"); + is_in_list(aManager, "addons://list/plugin", false, false); + + BrowserTestUtils.startLoadingURIString(gBrowser, "http://example.com/"); + await wait_for_page_load(gBrowser.selectedBrowser); + + info("Part 2"); + + await new Promise(resolve => + executeSoon(function () { + ok(gBrowser.canGoBack, "Should be able to go back"); + ok(!gBrowser.canGoForward, "Should not be able to go forward"); + + go_back(); + + gBrowser.addEventListener("pageshow", async function listener(event) { + if (event.target.location != "about:addons") { + return; + } + gBrowser.removeEventListener("pageshow", listener); + + aManager = await wait_for_view_load( + gBrowser.contentWindow.wrappedJSObject + ); + info("Part 3"); + is_in_list(aManager, "addons://list/plugin", false, true); + + executeSoon(() => go_forward()); + wait_for_page_load(gBrowser.selectedBrowser).then(() => { + info("Part 4"); + + executeSoon(function () { + ok(gBrowser.canGoBack, "Should be able to go back"); + ok(!gBrowser.canGoForward, "Should not be able to go forward"); + + go_back(); + + gBrowser.addEventListener( + "pageshow", + async function listener(event) { + if (event.target.location != "about:addons") { + return; + } + gBrowser.removeEventListener("pageshow", listener); + aManager = await wait_for_view_load( + gBrowser.contentWindow.wrappedJSObject + ); + info("Part 5"); + is_in_list(aManager, "addons://list/plugin", false, true); + + resolve(); + } + ); + }); + }); + }); + }) + ); + + await close_manager(aManager); + await SpecialPowers.popPrefEnv(); +}); + +// Tests that refreshing a list view does not affect the history +add_task(async function test_refresh_listview_donot_add_history_entries() { + let aManager = await open_manager("addons://list/extension"); + let categoryUtils = new CategoryUtilities(aManager); + info("Part 1"); + is_in_list(aManager, "addons://list/extension", false, false); + + EventUtils.synthesizeMouseAtCenter(categoryUtils.get("plugin"), {}, aManager); + + aManager = await wait_for_view_load(aManager); + info("Part 2"); + is_in_list(aManager, "addons://list/plugin", true, false); + + await new Promise(resolve => { + gBrowser.reload(); + gBrowser.addEventListener("pageshow", async function listener(event) { + if (event.target.location != "about:addons") { + return; + } + gBrowser.removeEventListener("pageshow", listener); + + aManager = await wait_for_view_load( + gBrowser.contentWindow.wrappedJSObject + ); + info("Part 3"); + is_in_list(aManager, "addons://list/plugin", true, false); + + go_back(); + aManager = await wait_for_view_load(aManager); + info("Part 4"); + is_in_list(aManager, "addons://list/extension", false, true); + resolve(); + }); + }); + + await close_manager(aManager); +}); + +// Tests that refreshing a detail view does not affect the history +add_task(async function test_refresh_detailview_donot_add_history_entries() { + let aManager = await open_manager(null); + info("Part 1"); + is_in_list(aManager, "addons://list/extension", false, false); + + await expand_addon_element(aManager, "test1@tests.mozilla.org"); + + aManager = await wait_for_view_load(aManager); + info("Part 2"); + is_in_detail(aManager, "addons://list/extension", true, false); + + await new Promise(resolve => { + gBrowser.reload(); + gBrowser.addEventListener("pageshow", async function listener(event) { + if (event.target.location != "about:addons") { + return; + } + gBrowser.removeEventListener("pageshow", listener); + + aManager = await wait_for_view_load( + gBrowser.contentWindow.wrappedJSObject + ); + info("Part 3"); + is_in_detail(aManager, "addons://list/extension", true, false); + + go_back(); + aManager = await wait_for_view_load(aManager); + info("Part 4"); + is_in_list(aManager, "addons://list/extension", false, true); + resolve(); + }); + }); + + await close_manager(aManager); +}); + +// Tests that removing an extension from the detail view goes back and doesn't +// allow you to go forward again. +add_task(async function test_history_on_detailview_extension_removed() { + let aManager = await open_manager("addons://list/extension"); + + info("Part 1"); + is_in_list(aManager, "addons://list/extension", false, false); + + await expand_addon_element(aManager, "test1@tests.mozilla.org"); + + aManager = await wait_for_view_load(aManager); + info("Part 2"); + is_in_detail(aManager, "addons://list/extension", true, false); + + const addonCard = aManager.document.querySelector( + 'addon-card[addon-id="test1@tests.mozilla.org"]' + ); + const promptService = mockPromptService(); + promptService._response = 0; + addonCard.querySelector("[action=remove]").click(); + + await wait_for_view_load(aManager); + await TestUtils.waitForCondition( + () => aManager.document.querySelector("addon-list"), + "The add-on list should render." + ); + is_in_list(aManager, "addons://list/extension", true, false); + + const addon = await AddonManager.getAddonByID("test1@tests.mozilla.org"); + addon.cancelUninstall(); + + await close_manager(aManager); +}); + +// Tests that opening the manager opens the last view +add_task(async function test_open_last_view() { + let aManager = await open_manager("addons://list/plugin"); + info("Part 1"); + is_in_list(aManager, "addons://list/plugin", false, false); + + await close_manager(aManager); + aManager = await open_manager(null); + info("Part 2"); + is_in_list(aManager, "addons://list/plugin", false, false); + + await close_manager(aManager); +}); + +// Tests that navigating the discovery page works when that was the first view +add_task(async function test_discopane_first_history_entry() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.getAddons.discovery.api_url", DISCOAPI_URL]], + }); + + let aManager = await open_manager("addons://discover/"); + let categoryUtils = new CategoryUtilities(aManager); + info("1"); + is_in_discovery(aManager, false, false); + + EventUtils.synthesizeMouseAtCenter(categoryUtils.get("plugin"), {}, aManager); + + aManager = await wait_for_view_load(aManager); + is_in_list(aManager, "addons://list/plugin", true, false); + + go_back(); + aManager = await wait_for_view_load(aManager); + + is_in_discovery(aManager, false, true); + + await close_manager(aManager); +}); + +// Tests that navigating the discovery page works when that was the second view +add_task(async function test_discopane_second_history_entry() { + let aManager = await open_manager("addons://list/plugin"); + let categoryUtils = new CategoryUtilities(aManager); + is_in_list(aManager, "addons://list/plugin", false, false); + + EventUtils.synthesizeMouseAtCenter( + categoryUtils.get("discover"), + {}, + aManager + ); + + aManager = await wait_for_view_load(aManager); + is_in_discovery(aManager, true, false); + + EventUtils.synthesizeMouseAtCenter(categoryUtils.get("plugin"), {}, aManager); + + aManager = await wait_for_view_load(aManager); + is_in_list(aManager, "addons://list/plugin", true, false); + + go_back(); + + aManager = await wait_for_view_load(aManager); + is_in_discovery(aManager, true, true); + + go_back(); + + aManager = await wait_for_view_load(aManager); + is_in_list(aManager, "addons://list/plugin", false, true); + + await close_manager(aManager); +}); + +add_task(async function test_initialSelectedView_on_aboutaddons_reload() { + let managerWindow = await open_manager("addons://list/extension"); + isnot( + managerWindow.gViewController.currentViewId, + null, + "Got a non null currentViewId on first load" + ); + + managerWindow.location.reload(); + await wait_for_manager_load(managerWindow); + await wait_for_view_load(managerWindow); + + isnot( + managerWindow.gViewController.currentViewId, + null, + "Got a non null currentViewId on reload" + ); + + await close_manager(managerWindow); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js b/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js new file mode 100644 index 0000000000..3ad8510aea --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js @@ -0,0 +1,1093 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* eslint max-len: ["error", 80] */ + +loadTestSubscript("head_abuse_report.js"); + +add_setup(async function () { + // Make sure the integrated abuse report panel is the one enabled + // while this test file runs (instead of the AMO hosted form). + // NOTE: behaviors expected when amoFormEnabled is true are tested + // in the separate browser_amo_abuse_report.js test file. + await SpecialPowers.pushPrefEnv({ + set: [["extensions.abuseReport.amoFormEnabled", false]], + }); + await AbuseReportTestUtils.setup(); +}); + +/** + * Base tests on abuse report panel webcomponents. + */ + +// This test case verified that the abuse report panels contains a radio +// button for all the expected "abuse report reasons", they are grouped +// together under the same form field named "reason". +add_task(async function test_abusereport_issuelist() { + const extension = await installTestExtension(); + + const abuseReportEl = await AbuseReportTestUtils.openReport(extension.id); + + const reasonsPanel = abuseReportEl._reasonsPanel; + const radioButtons = reasonsPanel.querySelectorAll("[type=radio]"); + const selectedRadios = reasonsPanel.querySelectorAll("[type=radio]:checked"); + + is(selectedRadios.length, 1, "Expect only one radio button selected"); + is( + selectedRadios[0], + radioButtons[0], + "Expect the first radio button to be selected" + ); + + is( + abuseReportEl.reason, + radioButtons[0].value, + `The reason property has the expected value: ${radioButtons[0].value}` + ); + + const reasons = Array.from(radioButtons).map(el => el.value); + Assert.deepEqual( + reasons.sort(), + AbuseReportTestUtils.getReasons(abuseReportEl).sort(), + `Got a radio button for the expected reasons` + ); + + for (const radio of radioButtons) { + const reasonInfo = AbuseReportTestUtils.getReasonInfo( + abuseReportEl, + radio.value + ); + const expectExampleHidden = + reasonInfo && reasonInfo.isExampleHidden("extension"); + is( + radio.parentNode.querySelector(".reason-example").hidden, + expectExampleHidden, + `Got expected visibility on the example for reason "${radio.value}"` + ); + } + + info("Change the selected reason to " + radioButtons[3].value); + radioButtons[3].checked = true; + is( + abuseReportEl.reason, + radioButtons[3].value, + "The reason property has the expected value" + ); + + await extension.unload(); + await closeAboutAddons(); +}); + +// This test case verifies that the abuse report panel: +// - switches from its "reasons list" mode to its "submit report" mode when the +// "next" button is clicked +// - goes back to the "reasons list" mode when the "go back" button is clicked +// - the abuse report panel is closed when the "close" icon is clicked +add_task(async function test_abusereport_submitpanel() { + const extension = await installTestExtension(); + + const abuseReportEl = await AbuseReportTestUtils.openReport(extension.id); + + ok( + !abuseReportEl._reasonsPanel.hidden, + "The list of abuse reasons is the currently visible" + ); + ok( + abuseReportEl._submitPanel.hidden, + "The submit panel is the currently hidden" + ); + + let onceUpdated = AbuseReportTestUtils.promiseReportUpdated( + abuseReportEl, + "submit" + ); + const MozButtonGroup = + abuseReportEl.ownerGlobal.customElements.get("moz-button-group"); + + ok(MozButtonGroup, "Expect MozButtonGroup custom element to be defined"); + + const assertButtonInMozButtonGroup = ( + btnEl, + { expectPrimary = false } = {} + ) => { + // Let's include the l10n id into the assertion messages, + // to make it more likely to be immediately clear which + // button hit a failure if any of the following assertion + // fails. + let l10nId = btnEl.getAttribute("data-l10n-id"); + is( + btnEl.classList.contains("primary"), + expectPrimary, + `Expect button ${l10nId} to have${ + expectPrimary ? "" : " NOT" + } the primary class set` + ); + + ok( + btnEl.parentElement instanceof MozButtonGroup, + `Expect button ${l10nId} to be slotted inside the expected custom element` + ); + + is( + btnEl.getAttribute("slot"), + expectPrimary ? "primary" : null, + `Expect button ${l10nId} slot to ${ + expectPrimary ? "" : "NOT " + } be set to primary` + ); + }; + + // Verify button group from the initial panel. + assertButtonInMozButtonGroup(abuseReportEl._btnNext, { expectPrimary: true }); + assertButtonInMozButtonGroup(abuseReportEl._btnCancel, { + expectPrimary: false, + }); + await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnNext); + await onceUpdated; + // Verify button group from the submit panel mode. + assertButtonInMozButtonGroup(abuseReportEl._btnSubmit, { + expectPrimary: true, + }); + assertButtonInMozButtonGroup(abuseReportEl._btnGoBack, { + expectPrimary: false, + }); + onceUpdated = AbuseReportTestUtils.promiseReportUpdated( + abuseReportEl, + "reasons" + ); + await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnGoBack); + await onceUpdated; + + const onceReportClosed = + AbuseReportTestUtils.promiseReportClosed(abuseReportEl); + await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnCancel); + await onceReportClosed; + + await extension.unload(); + await closeAboutAddons(); +}); + +// This test case verifies that the abuse report panel sends the expected data +// in the "abuse-report:submit" event detail. +add_task(async function test_abusereport_submit() { + // Reset the timestamp of the last report between tests. + AbuseReporter._lastReportTimestamp = null; + const extension = await installTestExtension(); + + const abuseReportEl = await AbuseReportTestUtils.openReport(extension.id); + + ok( + !abuseReportEl._reasonsPanel.hidden, + "The list of abuse reasons is the currently visible" + ); + + let onceUpdated = AbuseReportTestUtils.promiseReportUpdated( + abuseReportEl, + "submit" + ); + await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnNext); + await onceUpdated; + + is(abuseReportEl.message, "", "The abuse report message is initially empty"); + + info("Test typing a message in the abuse report submit panel textarea"); + const typedMessage = "Description of the extension abuse report"; + + EventUtils.synthesizeComposition( + { + data: typedMessage, + type: "compositioncommit", + }, + abuseReportEl.ownerGlobal + ); + + is( + abuseReportEl.message, + typedMessage, + "Got the expected typed message in the abuse report" + ); + + const expectedDetail = { + addonId: extension.id, + }; + + const expectedReason = abuseReportEl.reason; + const expectedMessage = abuseReportEl.message; + + function handleSubmitRequest({ request, response }) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json", false); + response.write("{}"); + } + + let reportSubmitted; + const onReportSubmitted = AbuseReportTestUtils.promiseReportSubmitHandled( + ({ data, request, response }) => { + reportSubmitted = JSON.parse(data); + handleSubmitRequest({ request, response }); + } + ); + + const onceReportClosed = + AbuseReportTestUtils.promiseReportClosed(abuseReportEl); + + const onMessageBarsCreated = AbuseReportTestUtils.promiseMessageBars(2); + + const onceSubmitEvent = BrowserTestUtils.waitForEvent( + abuseReportEl, + "abuse-report:submit" + ); + await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnSubmit); + const submitEvent = await onceSubmitEvent; + + const actualDetail = { + addonId: submitEvent.detail.addonId, + }; + Assert.deepEqual( + actualDetail, + expectedDetail, + "Got the expected detail in the abuse-report:submit event" + ); + + ok( + submitEvent.detail.report, + "Got a report object in the abuse-report:submit event detail" + ); + + // Verify that, when the "abuse-report:submit" has been sent, + // the abuse report panel has been hidden, the report has been + // submitted and the expected message bar is created in the + // HTML about:addons page. + info("Wait the report to be submitted to the api server"); + await onReportSubmitted; + info("Wait the report panel to be closed"); + await onceReportClosed; + + is( + reportSubmitted.addon, + ADDON_ID, + "Got the expected addon in the submitted report" + ); + is( + reportSubmitted.reason, + expectedReason, + "Got the expected reason in the submitted report" + ); + is( + reportSubmitted.message, + expectedMessage, + "Got the expected message in the submitted report" + ); + is( + reportSubmitted.report_entry_point, + REPORT_ENTRY_POINT, + "Got the expected report_entry_point in the submitted report" + ); + + info("Waiting the expected message bars to be created"); + const barDetails = await onMessageBarsCreated; + is(barDetails.length, 2, "Expect two message bars to have been created"); + is( + barDetails[0].definitionId, + "submitting", + "Got a submitting message bar as expected" + ); + is( + barDetails[1].definitionId, + "submitted", + "Got a submitted message bar as expected" + ); + + await extension.unload(); + await closeAboutAddons(); +}); + +// This helper does verify that the abuse report panel contains the expected +// suggestions when the selected reason requires it (and urls are being set +// on the links elements included in the suggestions when expected). +async function test_abusereport_suggestions(addonId) { + const addon = await AddonManager.getAddonByID(addonId); + + const abuseReportEl = await AbuseReportTestUtils.openReport(addonId); + + const { + _btnNext, + _btnGoBack, + _reasonsPanel, + _submitPanel, + _submitPanel: { _suggestions }, + } = abuseReportEl; + + for (const reason of AbuseReportTestUtils.getReasons(abuseReportEl)) { + const reasonInfo = AbuseReportTestUtils.getReasonInfo( + abuseReportEl, + reason + ); + + // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based + // implementation is also removed. + const addonType = + addon.type === "sitepermission-deprecated" + ? "sitepermission" + : addon.type; + + if (reasonInfo.isReasonHidden(addonType)) { + continue; + } + + info(`Test suggestions for abuse reason "${reason}"`); + + // Select a reason with suggestions. + let radioEl = abuseReportEl.querySelector(`#abuse-reason-${reason}`); + ok(radioEl, `Found radio button for "${reason}"`); + radioEl.checked = true; + + // Make sure the element localization is completed before + // checking the content isn't empty. + await document.l10n.translateFragment(radioEl); + + // Verify each radio button has a non-empty localized string. + const localizedRadioContent = Array.from( + radioEl.closest("label").querySelectorAll("[data-l10n-id]") + ).filter(el => !el.hidden); + + for (let el of localizedRadioContent) { + isnot( + el.textContent, + "", + `Fluent string id '${el.getAttribute("data-l10n-id")}' missing` + ); + } + + // Switch to the submit form with the current reason radio selected. + let oncePanelUpdated = AbuseReportTestUtils.promiseReportUpdated( + abuseReportEl, + "submit" + ); + await AbuseReportTestUtils.clickPanelButton(_btnNext); + await oncePanelUpdated; + + const localizedSuggestionsContent = Array.from( + _suggestions.querySelectorAll("[data-l10n-id]") + ).filter(el => !el.hidden); + + is( + !_suggestions.hidden, + !!reasonInfo.hasSuggestions, + `Suggestions block has the expected visibility for "${reason}"` + ); + if (reasonInfo.hasSuggestions) { + ok( + !!localizedSuggestionsContent.length, + `Category suggestions should not be empty for "${reason}"` + ); + } else { + Assert.strictEqual( + localizedSuggestionsContent.length, + 0, + `Category suggestions should be empty for "${reason}"` + ); + } + + const extSupportLink = _suggestions.querySelector( + ".extension-support-link" + ); + if (extSupportLink) { + is( + extSupportLink.getAttribute("href"), + BASE_TEST_MANIFEST.homepage_url, + "Got the expected extension-support-url" + ); + } + + const learnMoreLinks = []; + learnMoreLinks.push( + ..._suggestions.querySelectorAll( + 'a[is="moz-support-link"], .abuse-policy-learnmore' + ) + ); + + if (learnMoreLinks.length) { + is( + _suggestions.querySelectorAll( + 'a[is="moz-support-link"]:not([support-page])' + ).length, + 0, + "Every SUMO link should point to a specific page" + ); + ok( + learnMoreLinks.every(el => el.getAttribute("target") === "_blank"), + "All the learn more links have target _blank" + ); + ok( + learnMoreLinks.every(el => el.hasAttribute("href")), + "All the learn more links have a url set" + ); + } + + oncePanelUpdated = AbuseReportTestUtils.promiseReportUpdated( + abuseReportEl, + "reasons" + ); + await AbuseReportTestUtils.clickPanelButton(_btnGoBack); + await oncePanelUpdated; + ok(!_reasonsPanel.hidden, "Reasons panel should be visible"); + ok(_submitPanel.hidden, "Submit panel should be hidden"); + } + + await closeAboutAddons(); +} + +add_task(async function test_abusereport_suggestions_extension() { + const EXT_ID = "test-extension-suggestions@mochi.test"; + const extension = await installTestExtension(EXT_ID); + await test_abusereport_suggestions(EXT_ID); + await extension.unload(); +}); + +add_task(async function test_abusereport_suggestions_theme() { + const THEME_ID = "theme@mochi.test"; + const theme = await installTestExtension(THEME_ID, "theme"); + await test_abusereport_suggestions(THEME_ID); + await theme.unload(); +}); + +// TODO(Bug 1789718): adapt to SitePermAddonProvider implementation. +add_task(async function test_abusereport_suggestions_sitepermission() { + const SITEPERM_ADDON_ID = "webmidi@mochi.test"; + const sitePermAddon = await installTestExtension( + SITEPERM_ADDON_ID, + "sitepermission-deprecated" + ); + await test_abusereport_suggestions(SITEPERM_ADDON_ID); + await sitePermAddon.unload(); +}); + +// This test case verifies the message bars created on other +// scenarios (e.g. report creation and submissions errors). +// +// TODO(Bug 1789718): adapt to SitePermAddonProvider implementation. +add_task(async function test_abusereport_messagebars() { + const EXT_ID = "test-extension-report@mochi.test"; + const EXT_ID2 = "test-extension-report-2@mochi.test"; + const THEME_ID = "test-theme-report@mochi.test"; + const SITEPERM_ADDON_ID = "webmidi-report@mochi.test"; + const extension = await installTestExtension(EXT_ID); + const extension2 = await installTestExtension(EXT_ID2); + const theme = await installTestExtension(THEME_ID, "theme"); + const sitePermAddon = await installTestExtension( + SITEPERM_ADDON_ID, + "sitepermission-deprecated" + ); + + async function assertMessageBars( + expectedMessageBarIds, + testSetup, + testMessageBarDetails + ) { + await openAboutAddons(); + const expectedLength = expectedMessageBarIds.length; + const onMessageBarsCreated = + AbuseReportTestUtils.promiseMessageBars(expectedLength); + // Reset the timestamp of the last report between tests. + AbuseReporter._lastReportTimestamp = null; + await testSetup(); + info(`Waiting for ${expectedLength} message-bars to be created`); + const barDetails = await onMessageBarsCreated; + Assert.deepEqual( + barDetails.map(d => d.definitionId), + expectedMessageBarIds, + "Got the expected message bars" + ); + if (testMessageBarDetails) { + await testMessageBarDetails(barDetails); + } + await closeAboutAddons(); + } + + function setTestRequestHandler(responseStatus, responseData) { + AbuseReportTestUtils.promiseReportSubmitHandled(({ request, response }) => { + response.setStatusLine(request.httpVersion, responseStatus, "Error"); + response.write(responseData); + }); + } + + await assertMessageBars(["ERROR_ADDON_NOTFOUND"], async () => { + info("Test message bars on addon not found"); + AbuseReportTestUtils.triggerNewReport( + "non-existend-addon-id@mochi.test", + REPORT_ENTRY_POINT + ); + }); + + await assertMessageBars(["submitting", "ERROR_RECENT_SUBMIT"], async () => { + info("Test message bars on recent submission"); + const promiseRendered = AbuseReportTestUtils.promiseReportRendered(); + AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT); + await promiseRendered; + AbuseReporter.updateLastReportTimestamp(); + AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); + }); + + await assertMessageBars(["submitting", "ERROR_ABORTED_SUBMIT"], async () => { + info("Test message bars on aborted submission"); + AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT); + await AbuseReportTestUtils.promiseReportRendered(); + const { _report } = AbuseReportTestUtils.getReportPanel(); + _report.abort(); + AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); + }); + + await assertMessageBars(["submitting", "ERROR_SERVER"], async () => { + info("Test message bars on server error"); + setTestRequestHandler(500); + AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT); + await AbuseReportTestUtils.promiseReportRendered(); + AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); + }); + + await assertMessageBars(["submitting", "ERROR_CLIENT"], async () => { + info("Test message bars on client error"); + setTestRequestHandler(400); + AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT); + await AbuseReportTestUtils.promiseReportRendered(); + AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); + }); + + await assertMessageBars(["submitting", "ERROR_UNKNOWN"], async () => { + info("Test message bars on unexpected status code"); + setTestRequestHandler(604); + AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT); + await AbuseReportTestUtils.promiseReportRendered(); + AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); + }); + + await assertMessageBars(["submitting", "ERROR_UNKNOWN"], async () => { + info("Test message bars on invalid json in the response data"); + setTestRequestHandler(200, ""); + AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT); + await AbuseReportTestUtils.promiseReportRendered(); + AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); + }); + + // Verify message bar on add-on without perm_can_uninstall. + await assertMessageBars( + ["submitting", "submitted-no-remove-action"], + async () => { + info("Test message bars on report submitted on an addon without remove"); + setTestRequestHandler(200, "{}"); + AbuseReportTestUtils.triggerNewReport(THEME_NO_UNINSTALL_ID, "menu"); + await AbuseReportTestUtils.promiseReportRendered(); + AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); + } + ); + + // Verify the 3 expected entry points: + // menu, toolbar_context_menu and uninstall + // (See https://addons-server.readthedocs.io/en/latest/topics/api/abuse.html). + await assertMessageBars(["submitting", "submitted"], async () => { + info("Test message bars on report opened from addon options menu"); + setTestRequestHandler(200, "{}"); + AbuseReportTestUtils.triggerNewReport(EXT_ID, "menu"); + await AbuseReportTestUtils.promiseReportRendered(); + AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); + }); + + for (const extId of [EXT_ID, THEME_ID]) { + await assertMessageBars( + ["submitting", "submitted"], + async () => { + info(`Test message bars on ${extId} reported from toolbar contextmenu`); + setTestRequestHandler(200, "{}"); + AbuseReportTestUtils.triggerNewReport(extId, "toolbar_context_menu"); + await AbuseReportTestUtils.promiseReportRendered(); + AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); + }, + ([submittingDetails, submittedDetails]) => { + const buttonsL10nId = Array.from( + submittedDetails.messagebar.querySelectorAll("button") + ).map(el => el.getAttribute("data-l10n-id")); + if (extId === THEME_ID) { + ok( + buttonsL10nId.every(id => id.endsWith("-theme")), + "submitted bar actions should use the Fluent id for themes" + ); + } else { + ok( + buttonsL10nId.every(id => id.endsWith("-extension")), + "submitted bar actions should use the Fluent id for extensions" + ); + } + } + ); + } + + for (const extId of [EXT_ID2, THEME_ID, SITEPERM_ADDON_ID]) { + const testFn = async () => { + info(`Test message bars on ${extId} reported opened from addon removal`); + setTestRequestHandler(200, "{}"); + AbuseReportTestUtils.triggerNewReport(extId, "uninstall"); + await AbuseReportTestUtils.promiseReportRendered(); + const addon = await AddonManager.getAddonByID(extId); + // Ensure that the test extension is pending uninstall as it would be + // when a user trigger this scenario on an actual addon uninstall. + await addon.uninstall(true); + AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); + }; + const assertMessageBarDetails = async ([ + submittingDetails, + submittedDetails, + ]) => AbuseReportTestUtils.assertFluentStrings(submittedDetails.messagebar); + await assertMessageBars( + ["submitting", "submitted-and-removed"], + testFn, + assertMessageBarDetails + ); + } + + // Verify message bar on sitepermission add-on type. + await assertMessageBars( + ["submitting", "submitted"], + async () => { + info( + "Test message bars for report submitted on an sitepermission addon type" + ); + setTestRequestHandler(200, "{}"); + AbuseReportTestUtils.triggerNewReport(SITEPERM_ADDON_ID, "menu"); + await AbuseReportTestUtils.promiseReportRendered(); + AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); + }, + ([submittingDetails, submittedDetails]) => + AbuseReportTestUtils.assertFluentStrings(submittedDetails.messagebar) + ); + + await extension.unload(); + await extension2.unload(); + await theme.unload(); + await sitePermAddon.unload(); +}); + +add_task(async function test_abusereport_from_aboutaddons_menu() { + const EXT_ID = "test-report-from-aboutaddons-menu@mochi.test"; + const extension = await installTestExtension(EXT_ID); + + await openAboutAddons(); + + AbuseReportTestUtils.assertReportPanelHidden(); + + const addonCard = gManagerWindow.document.querySelector( + `addon-list addon-card[addon-id="${extension.id}"]` + ); + ok(addonCard, "Got the addon-card for the test extension"); + + const reportButton = addonCard.querySelector("[action=report]"); + ok(reportButton, "Got the report action for the test extension"); + + info("Click the report action and wait for the 'abuse-report:new' event"); + + let onceReportOpened = AbuseReportTestUtils.promiseReportOpened({ + addonId: extension.id, + reportEntryPoint: "menu", + }); + reportButton.click(); + const panelEl = await onceReportOpened; + + await AbuseReportTestUtils.closeReportPanel(panelEl); + + await closeAboutAddons(); + await extension.unload(); +}); + +add_task(async function test_abusereport_from_aboutaddons_remove() { + const EXT_ID = "test-report-from-aboutaddons-remove@mochi.test"; + + // Test on a theme addon to cover the report checkbox included in the + // uninstall dialog also on a theme. + const extension = await installTestExtension(EXT_ID, "theme"); + + await openAboutAddons("theme"); + + AbuseReportTestUtils.assertReportPanelHidden(); + + const addonCard = gManagerWindow.document.querySelector( + `addon-list addon-card[addon-id="${extension.id}"]` + ); + ok(addonCard, "Got the addon-card for the test theme extension"); + + const removeButton = addonCard.querySelector("[action=remove]"); + ok(removeButton, "Got the remove action for the test theme extension"); + + // Prepare the mocked prompt service. + const promptService = mockPromptService(); + promptService.confirmEx = createPromptConfirmEx({ + remove: true, + report: true, + }); + + info("Click the report action and wait for the 'abuse-report:new' event"); + + const onceReportOpened = AbuseReportTestUtils.promiseReportOpened({ + addonId: extension.id, + reportEntryPoint: "uninstall", + }); + removeButton.click(); + const panelEl = await onceReportOpened; + + await AbuseReportTestUtils.closeReportPanel(panelEl); + + await closeAboutAddons(); + await extension.unload(); +}); + +add_task(async function test_abusereport_from_browserAction_remove() { + const EXT_ID = "test-report-from-browseraction-remove@mochi.test"; + const xpiFile = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + ...BASE_TEST_MANIFEST, + browser_action: { + default_area: "navbar", + }, + browser_specific_settings: { gecko: { id: EXT_ID } }, + }, + }); + const addon = await AddonManager.installTemporaryAddon(xpiFile); + + const buttonId = `${makeWidgetId(EXT_ID)}-browser-action`; + + async function promiseAnimationFrame() { + await new Promise(resolve => window.requestAnimationFrame(resolve)); + + let { tm } = Services; + return new Promise(resolve => tm.dispatchToMainThread(resolve)); + } + + async function reportFromContextMenuRemove() { + const menu = document.getElementById("toolbar-context-menu"); + const node = document.getElementById(CSS.escape(buttonId)); + const shown = BrowserTestUtils.waitForEvent( + menu, + "popupshown", + "Wair for contextmenu popup" + ); + + // Wait for an animation frame as we do for the other mochitest-browser + // tests related to the browserActions. + await promiseAnimationFrame(); + EventUtils.synthesizeMouseAtCenter(node, { type: "contextmenu" }); + await shown; + + info(`Clicking on "Remove Extension" context menu item`); + let removeExtension = menu.querySelector( + ".customize-context-removeExtension" + ); + removeExtension.click(); + + return menu; + } + + // Prepare the mocked prompt service. + const promptService = mockPromptService(); + promptService.confirmEx = createPromptConfirmEx({ + remove: true, + report: true, + }); + + await BrowserTestUtils.withNewTab("about:blank", async function () { + info(`Open browserAction context menu in toolbar context menu`); + let promiseMenu = reportFromContextMenuRemove(); + + // Wait about:addons to be loaded. + let browser = gBrowser.selectedBrowser; + await BrowserTestUtils.browserLoaded(browser); + + let onceReportOpened = AbuseReportTestUtils.promiseReportOpened({ + addonId: EXT_ID, + reportEntryPoint: "uninstall", + managerWindow: browser.contentWindow, + }); + + is( + browser.currentURI.spec, + "about:addons", + "about:addons tab currently selected" + ); + + let menu = await promiseMenu; + menu.hidePopup(); + + let panelEl = await onceReportOpened; + + await AbuseReportTestUtils.closeReportPanel(panelEl); + + let onceExtStarted = AddonTestUtils.promiseWebExtensionStartup(EXT_ID); + addon.cancelUninstall(); + await onceExtStarted; + + // Reload the tab to verify Bug 1559124 didn't regressed. + browser.contentWindow.location.reload(); + await BrowserTestUtils.browserLoaded(browser); + is( + browser.currentURI.spec, + "about:addons", + "about:addons tab currently selected" + ); + + onceReportOpened = AbuseReportTestUtils.promiseReportOpened({ + addonId: EXT_ID, + reportEntryPoint: "uninstall", + managerWindow: browser.contentWindow, + }); + + menu = await reportFromContextMenuRemove(); + info("Wait for the report panel"); + panelEl = await onceReportOpened; + + info("Wait for the report panel to be closed"); + await AbuseReportTestUtils.closeReportPanel(panelEl); + + menu.hidePopup(); + + onceExtStarted = AddonTestUtils.promiseWebExtensionStartup(EXT_ID); + addon.cancelUninstall(); + await onceExtStarted; + }); + + await addon.uninstall(); +}); + +/* + * Test report action hidden on non-supported extension types. + */ +add_task(async function test_report_action_hidden_on_builtin_addons() { + await openAboutAddons("theme"); + await AbuseReportTestUtils.assertReportActionHidden( + gManagerWindow, + DEFAULT_BUILTIN_THEME_ID + ); + await closeAboutAddons(); +}); + +add_task(async function test_report_action_hidden_on_system_addons() { + await openAboutAddons("extension"); + await AbuseReportTestUtils.assertReportActionHidden( + gManagerWindow, + EXT_SYSTEM_ADDON_ID + ); + await closeAboutAddons(); +}); + +add_task(async function test_report_action_hidden_on_dictionary_addons() { + await openAboutAddons("dictionary"); + await AbuseReportTestUtils.assertReportActionHidden( + gManagerWindow, + EXT_DICTIONARY_ADDON_ID + ); + await closeAboutAddons(); +}); + +add_task(async function test_report_action_hidden_on_langpack_addons() { + await openAboutAddons("locale"); + await AbuseReportTestUtils.assertReportActionHidden( + gManagerWindow, + EXT_LANGPACK_ADDON_ID + ); + await closeAboutAddons(); +}); + +// This test verifies that triggering a report that would be immediately +// cancelled (e.g. because abuse reports for that extension type are not +// supported) the abuse report is being hidden as expected. +add_task(async function test_report_hidden_on_report_unsupported_addontype() { + await openAboutAddons(); + + let onceCreateReportFailed = AbuseReportTestUtils.promiseMessageBars(1); + + AbuseReportTestUtils.triggerNewReport(EXT_UNSUPPORTED_TYPE_ADDON_ID, "menu"); + + await onceCreateReportFailed; + + ok(!AbuseReporter.getOpenDialog(), "report dialog should not be open"); + + await closeAboutAddons(); +}); + +/* + * Test regression fixes. + */ + +add_task(async function test_no_broken_suggestion_on_missing_supportURL() { + const EXT_ID = "test-no-author@mochi.test"; + const extension = await installTestExtension(EXT_ID, "extension", { + homepage_url: undefined, + }); + + const abuseReportEl = await AbuseReportTestUtils.openReport(EXT_ID); + + info("Select broken as the abuse reason"); + abuseReportEl.querySelector("#abuse-reason-broken").checked = true; + + let oncePanelUpdated = AbuseReportTestUtils.promiseReportUpdated( + abuseReportEl, + "submit" + ); + await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnNext); + await oncePanelUpdated; + + const suggestionEl = abuseReportEl.querySelector( + "abuse-report-reason-suggestions" + ); + is(suggestionEl.reason, "broken", "Got the expected suggestion element"); + ok(suggestionEl.hidden, "suggestion element should be empty"); + + await closeAboutAddons(); + await extension.unload(); +}); + +// This test verify that the abuse report panel is opening the +// author link using a null triggeringPrincipal. +add_task(async function test_abusereport_open_author_url() { + const abuseReportEl = await AbuseReportTestUtils.openReport( + EXT_WITH_PRIVILEGED_URL_ID + ); + + const authorLink = abuseReportEl._linkAddonAuthor; + ok(authorLink, "Got the author link element"); + is( + authorLink.href, + "about:config", + "Got a privileged url in the link element" + ); + + SimpleTest.waitForExplicitFinish(); + let waitForConsole = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, [ + { + message: + // eslint-disable-next-line max-len + /Security Error: Content at moz-nullprincipal:{.*} may not load or link to about:config/, + }, + ]); + }); + + let tabSwitched = BrowserTestUtils.waitForEvent(gBrowser, "TabSwitchDone"); + authorLink.click(); + await tabSwitched; + + is( + gBrowser.selectedBrowser.currentURI.spec, + "about:blank", + "Got about:blank loaded in the new tab" + ); + + SimpleTest.endMonitorConsole(); + await waitForConsole; + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await closeAboutAddons(); +}); + +add_task(async function test_no_report_checkbox_for_unsupported_addon_types() { + async function test_report_checkbox_hidden(addon) { + await openAboutAddons(addon.type); + + const addonCard = gManagerWindow.document.querySelector( + `addon-list addon-card[addon-id="${addon.id}"]` + ); + ok(addonCard, "Got the addon-card for the test extension"); + + const removeButton = addonCard.querySelector("[action=remove]"); + ok(removeButton, "Got the remove action for the test extension"); + + // Prepare the mocked prompt service. + const promptService = mockPromptService(); + promptService.confirmEx = createPromptConfirmEx({ + remove: true, + report: false, + expectCheckboxHidden: true, + }); + + info("Click the report action and wait for the addon to be removed"); + const promiseCardRemoved = BrowserTestUtils.waitForEvent( + addonCard.closest("addon-list"), + "remove" + ); + removeButton.click(); + await promiseCardRemoved; + + await closeAboutAddons(); + } + + const reportNotSupportedAddons = [ + { + id: "fake-langpack-to-remove@mochi.test", + name: "This is a fake langpack", + version: "1.1", + type: "locale", + }, + { + id: "fake-dictionary-to-remove@mochi.test", + name: "This is a fake dictionary", + version: "1.1", + type: "dictionary", + }, + ]; + + AbuseReportTestUtils.createMockAddons(reportNotSupportedAddons); + + for (const { id } of reportNotSupportedAddons) { + const addon = await AddonManager.getAddonByID(id); + await test_report_checkbox_hidden(addon); + } +}); + +add_task(async function test_author_hidden_when_missing() { + const EXT_ID = "test-no-author@mochi.test"; + const extension = await installTestExtension(EXT_ID, "extension", { + author: undefined, + }); + + const abuseReportEl = await AbuseReportTestUtils.openReport(EXT_ID); + + const addon = await AddonManager.getAddonByID(EXT_ID); + + ok(!addon.creator, "addon.creator should not be undefined"); + ok( + abuseReportEl._addonAuthorContainer.hidden, + "author container should be hidden" + ); + + await closeAboutAddons(); + await extension.unload(); +}); + +// Verify addon.siteOrigin is used as a fallback when homepage_url/developer.url +// or support url are missing. +// +// TODO(Bug 1789718): adapt to SitePermAddonProvider implementation. +add_task(async function test_siteperm_siteorigin_fallback() { + const SITEPERM_ADDON_ID = "webmidi-site-origin@mochi.test"; + const sitePermAddon = await installTestExtension( + SITEPERM_ADDON_ID, + "sitepermission-deprecated", + { + homepage_url: undefined, + } + ); + + const abuseReportEl = await AbuseReportTestUtils.openReport( + SITEPERM_ADDON_ID + ); + const addon = await AddonManager.getAddonByID(SITEPERM_ADDON_ID); + + ok(addon.siteOrigin, "addon.siteOrigin should not be undefined"); + ok(!addon.supportURL, "addon.supportURL should not be set"); + ok(!addon.homepageURL, "addon.homepageURL should not be set"); + is( + abuseReportEl.supportURL, + addon.siteOrigin, + "Got the expected support_url" + ); + + await closeAboutAddons(); + await sitePermAddon.unload(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report_dialog.js b/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report_dialog.js new file mode 100644 index 0000000000..1efb28add3 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report_dialog.js @@ -0,0 +1,185 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* eslint max-len: ["error", 80] */ + +loadTestSubscript("head_abuse_report.js"); + +add_setup(async function () { + // Make sure the integrated abuse report panel is the one enabled + // while this test file runs (instead of the AMO hosted form). + // NOTE: behaviors expected when amoFormEnabled is true are tested + // in the separate browser_amo_abuse_report.js test file. + await SpecialPowers.pushPrefEnv({ + set: [["extensions.abuseReport.amoFormEnabled", false]], + }); + await AbuseReportTestUtils.setup(); +}); + +/** + * Test tasks specific to the abuse report opened in its own dialog window. + */ + +add_task(async function test_close_icon_button_hidden_when_dialog() { + const addonId = "addon-to-report@mochi.test"; + const extension = await installTestExtension(addonId); + + const reportDialog = await AbuseReporter.openDialog( + addonId, + "menu", + gBrowser.selectedBrowser + ); + await AbuseReportTestUtils.promiseReportDialogRendered(); + + const panelEl = await reportDialog.promiseReportPanel; + + let promiseClosedWindow = waitClosedWindow(); + + EventUtils.synthesizeKey("VK_RETURN", {}, panelEl.ownerGlobal); + AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); + + await promiseClosedWindow; + ok( + await reportDialog.promiseReport, + "expect the report to not be cancelled by pressing enter" + ); + + await extension.unload(); +}); + +add_task(async function test_report_triggered_when_report_dialog_is_open() { + const addonId = "addon-to-report@mochi.test"; + const extension = await installTestExtension(addonId); + + const reportDialog = await AbuseReporter.openDialog( + addonId, + "menu", + gBrowser.selectedBrowser + ); + await AbuseReportTestUtils.promiseReportDialogRendered(); + + let promiseClosedWindow = waitClosedWindow(); + + const reportDialog2 = await AbuseReporter.openDialog( + addonId, + "menu", + gBrowser.selectedBrowser + ); + + await promiseClosedWindow; + + // Trigger the report submit and check that the second report is + // resolved as expected. + await AbuseReportTestUtils.promiseReportDialogRendered(); + + ok( + !reportDialog.window || reportDialog.window.closed, + "expect the first dialog to be closed" + ); + ok(!!reportDialog2.window, "expect the second dialog to be open"); + + is( + reportDialog2.window, + AbuseReportTestUtils.getReportDialog(), + "Got a report dialog as expected" + ); + + AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); + + // promiseReport is resolved to undefined if the report has been + // cancelled, otherwise it is resolved to a report object. + ok( + !(await reportDialog.promiseReport), + "expect the first report to be cancelled" + ); + ok( + !!(await reportDialog2.promiseReport), + "expect the second report to be resolved" + ); + + await extension.unload(); +}); + +add_task(async function test_report_dialog_window_closed_by_user() { + const addonId = "addon-to-report@mochi.test"; + const extension = await installTestExtension(addonId); + + const reportDialog = await AbuseReporter.openDialog( + addonId, + "menu", + gBrowser.selectedBrowser + ); + await AbuseReportTestUtils.promiseReportDialogRendered(); + + let promiseClosedWindow = waitClosedWindow(); + + reportDialog.close(); + + await promiseClosedWindow; + + ok( + !(await reportDialog.promiseReport), + "expect promiseReport to be resolved as user cancelled" + ); + + await extension.unload(); +}); + +add_task(async function test_amo_details_for_not_installed_addon() { + const addonId = "not-installed-addon@mochi.test"; + const fakeAMODetails = { + name: "fake name", + current_version: { version: "1.0" }, + type: "extension", + icon_url: "http://test.addons.org/asserts/fake-icon-url.png", + homepage: "http://fake.url/homepage", + support_url: "http://fake.url/support", + authors: [ + { name: "author1", url: "http://fake.url/author1" }, + { name: "author2", url: "http://fake.url/author2" }, + ], + is_recommended: true, + }; + + AbuseReportTestUtils.amoAddonDetailsMap.set(addonId, fakeAMODetails); + registerCleanupFunction(() => + AbuseReportTestUtils.amoAddonDetailsMap.clear() + ); + + const reportDialog = await AbuseReporter.openDialog( + addonId, + "menu", + gBrowser.selectedBrowser + ); + + const reportEl = await reportDialog.promiseReportPanel; + + // Assert that the panel has been able to retrieve from AMO + // all the addon details needed to render the panel correctly. + is(reportEl.addonId, addonId, "Got the expected addonId"); + is(reportEl.addonName, fakeAMODetails.name, "Got the expected addon name"); + is(reportEl.addonType, fakeAMODetails.type, "Got the expected addon type"); + is( + reportEl.authorName, + fakeAMODetails.authors[0].name, + "Got the first author name as expected" + ); + is( + reportEl.authorURL, + fakeAMODetails.authors[0].url, + "Got the first author url as expected" + ); + is(reportEl.iconURL, fakeAMODetails.icon_url, "Got the expected icon url"); + is( + reportEl.supportURL, + fakeAMODetails.support_url, + "Got the expected support url" + ); + is( + reportEl.homepageURL, + fakeAMODetails.homepage, + "Got the expected homepage url" + ); + + reportDialog.close(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_detail_permissions.js b/toolkit/mozapps/extensions/test/browser/browser_html_detail_permissions.js new file mode 100644 index 0000000000..939fe421c3 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_detail_permissions.js @@ -0,0 +1,827 @@ +/* eslint max-len: ["error", 80] */ + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { ExtensionPermissions } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionPermissions.sys.mjs" +); +const { PERMISSION_L10N, PERMISSION_L10N_ID_OVERRIDES } = + ChromeUtils.importESModule( + "resource://gre/modules/ExtensionPermissionMessages.sys.mjs" + ); + +AddonTestUtils.initMochitest(this); + +async function background() { + browser.permissions.onAdded.addListener(perms => { + browser.test.sendMessage("permission-added", perms); + }); + browser.permissions.onRemoved.addListener(perms => { + browser.test.sendMessage("permission-removed", perms); + }); +} + +async function getExtensions({ manifest_version = 2 } = {}) { + let extensions = { + "addon0@mochi.test": ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version, + name: "Test add-on 0", + browser_specific_settings: { gecko: { id: "addon0@mochi.test" } }, + permissions: ["alarms", "contextMenus"], + }, + background, + useAddonManager: "temporary", + }), + "addon1@mochi.test": ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version, + name: "Test add-on 1", + browser_specific_settings: { gecko: { id: "addon1@mochi.test" } }, + permissions: ["alarms", "contextMenus", "tabs", "webNavigation"], + // Note: for easier testing, we merge host_permissions into permissions + // when loading mv2 extensions, see ExtensionTestCommon.generateFiles. + host_permissions: ["<all_urls>", "file://*/*"], + }, + background, + useAddonManager: "temporary", + }), + "addon2@mochi.test": ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version, + name: "Test add-on 2", + browser_specific_settings: { gecko: { id: "addon2@mochi.test" } }, + permissions: ["alarms", "contextMenus"], + optional_permissions: ["http://mochi.test/*"], + }, + background, + useAddonManager: "temporary", + }), + "addon3@mochi.test": ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version, + name: "Test add-on 3", + version: "1.0", + browser_specific_settings: { gecko: { id: "addon3@mochi.test" } }, + permissions: ["tabs"], + optional_permissions: ["webNavigation", "<all_urls>"], + }, + background, + useAddonManager: "temporary", + }), + "addon4@mochi.test": ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version, + name: "Test add-on 4", + browser_specific_settings: { gecko: { id: "addon4@mochi.test" } }, + optional_permissions: ["tabs", "webNavigation"], + }, + background, + useAddonManager: "temporary", + }), + "addon5@mochi.test": ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version, + name: "Test add-on 5", + browser_specific_settings: { gecko: { id: "addon5@mochi.test" } }, + optional_permissions: ["*://*/*"], + }, + background, + useAddonManager: "temporary", + }), + "priv6@mochi.test": ExtensionTestUtils.loadExtension({ + isPrivileged: true, + manifest: { + manifest_version, + name: "Privileged add-on 6", + browser_specific_settings: { gecko: { id: "priv6@mochi.test" } }, + optional_permissions: [ + "file://*/*", + "about:reader*", + "resource://pdf.js/*", + "*://*.mozilla.com/*", + "*://*/*", + "<all_urls>", + ], + }, + background, + useAddonManager: "temporary", + }), + "addon7@mochi.test": ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version, + name: "Test add-on 7", + browser_specific_settings: { gecko: { id: "addon7@mochi.test" } }, + optional_permissions: ["<all_urls>", "https://*/*", "file://*/*"], + }, + background, + useAddonManager: "temporary", + }), + "addon8@mochi.test": ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version, + name: "Test add-on 8", + browser_specific_settings: { gecko: { id: "addon8@mochi.test" } }, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + optional_permissions: ["https://*/*", "http://*/*", "file://*/*"], + }, + background, + useAddonManager: "temporary", + }), + "other@mochi.test": ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version, + name: "Test add-on 6", + browser_specific_settings: { gecko: { id: "other@mochi.test" } }, + optional_permissions: [ + "tabs", + "webNavigation", + "<all_urls>", + "*://*/*", + ], + }, + useAddonManager: "temporary", + }), + }; + for (let ext of Object.values(extensions)) { + await ext.startup(); + } + return extensions; +} + +async function runTest(options) { + let { + extension, + addonId, + permissions = [], + optional_permissions = [], + optional_overlapping = [], + optional_enabled = [], + // Map<permission->string> to check optional_permissions against, if set. + optional_strings = {}, + view, + } = options; + if (extension) { + addonId = extension.id; + } + + let win = view || (await loadInitialView("extension")); + + let card = getAddonCard(win, addonId); + let permsSection = card.querySelector("addon-permissions-list"); + if (!permsSection) { + ok(!card.hasAttribute("expanded"), "The list card is not expanded"); + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + } + + card = getAddonCard(win, addonId); + let { deck, tabGroup } = card.details; + + let permsBtn = tabGroup.querySelector('[name="permissions"]'); + let permsShown = BrowserTestUtils.waitForEvent(deck, "view-changed"); + permsBtn.click(); + await permsShown; + + permsSection = card.querySelector("addon-permissions-list"); + + let rows = Array.from(permsSection.querySelectorAll(".addon-detail-row")); + let permission_rows = Array.from( + permsSection.querySelectorAll(".permission-info") + ); + + // Last row is the learn more link. + info("Check learn more link"); + let link = rows[rows.length - 1].firstElementChild; + let rootUrl = Services.urlFormatter.formatURLPref("app.support.baseURL"); + let url = rootUrl + "extension-permissions"; + is(link.href, url, "The URL is set"); + is(link.getAttribute("target"), "_blank", "The link opens in a new tab"); + + // We should have one more row (learn more) that the combined permissions, + // or if no permissions, 2 rows. + let num_permissions = permissions.length + optional_permissions.length; + is( + permission_rows.length, + num_permissions, + "correct number of details rows are present" + ); + + info("Check displayed permissions"); + if (!num_permissions) { + is( + win.document.l10n.getAttributes(rows[0]).id, + "addon-permissions-empty", + "There's a message when no permissions are shown" + ); + } + if (permissions.length) { + for (let name of permissions) { + // Check the permission-info class to make sure it's for a permission. + let row = permission_rows.shift(); + ok( + row.classList.contains("permission-info"), + `required permission row for ${name}` + ); + } + } + + let addon = await AddonManager.getAddonByID(addonId); + info(`addon ${addon.id} is ${addon.userDisabled ? "disabled" : "enabled"}`); + + function waitForPermissionChange(id) { + return new Promise(resolve => { + info(`listening for change on ${id}`); + let listener = (type, data) => { + info(`change permissions ${JSON.stringify(data)}`); + if (data.extensionId !== id) { + return; + } + ExtensionPermissions.removeListener(listener); + resolve(data); + }; + ExtensionPermissions.addListener(listener); + }); + } + + // This tests the permission change and button state when the user + // changes the state in about:addons. + async function testTogglePermissionButton( + permissions, + button, + excpectDisabled = false + ) { + let enabled = permissions.some(perm => optional_enabled.includes(perm)); + if (excpectDisabled) { + enabled = !enabled; + } + is( + button.pressed, + enabled, + `permission is set correctly for ${permissions}: ${button.pressed}` + ); + let change; + if (addon.userDisabled || !extension) { + change = waitForPermissionChange(addonId); + } else if (!enabled) { + change = extension.awaitMessage("permission-added"); + } else { + change = extension.awaitMessage("permission-removed"); + } + + button.click(); + + let perms = await change; + if (addon.userDisabled || !extension) { + perms = enabled ? perms.removed : perms.added; + } + + Assert.greater( + perms.permissions.length + perms.origins.length, + 0, + "Some permission(s) toggled." + ); + + if (perms.permissions.length) { + // Only check api permissions against the first passed permission, + // because we treat <all_urls> as an api permission, but not *://*/*. + is(perms.permissions.length, 1, "A single api permission toggled."); + is(perms.permissions[0], permissions[0], "Correct api permission."); + } + if (perms.origins.length) { + Assert.deepEqual( + perms.origins.slice().sort(), + permissions.slice().sort(), + "Toggled origin permission." + ); + } + + await BrowserTestUtils.waitForCondition(async () => { + return button.pressed == !enabled; + }, "button changed state"); + } + + // This tests that the button changes state if the permission is + // changed outside of about:addons + async function testExternalPermissionChange(permission, button) { + let enabled = button.pressed; + let type = button.getAttribute("permission-type"); + let change; + if (addon.userDisabled || !extension) { + change = waitForPermissionChange(addonId); + } else if (!enabled) { + change = extension.awaitMessage("permission-added"); + } else { + change = extension.awaitMessage("permission-removed"); + } + + let permissions = { permissions: [], origins: [] }; + if (type == "origin") { + permissions.origins = [permission]; + } else { + permissions.permissions = [permission]; + } + + if (enabled) { + await ExtensionPermissions.remove(addonId, permissions); + } else { + await ExtensionPermissions.add(addonId, permissions); + } + + let perms = await change; + if (addon.userDisabled || !extension) { + perms = enabled ? perms.removed : perms.added; + } + ok( + perms.permissions.includes(permission) || + perms.origins.includes(permission), + "permission was toggled" + ); + + await BrowserTestUtils.waitForCondition(async () => { + return button.pressed == !enabled; + }, "button changed state"); + } + + // This tests that changing the permission on another addon does + // not change the UI for the addon we're testing. + async function testOtherPermissionChange(permission, toggle) { + let type = toggle.getAttribute("permission-type"); + let otherId = "other@mochi.test"; + let change = waitForPermissionChange(otherId); + let perms = await ExtensionPermissions.get(otherId); + let existing = type == "origin" ? perms.origins : perms.permissions; + let permissions = { permissions: [], origins: [] }; + if (type == "origin") { + permissions.origins = [permission]; + } else { + permissions.permissions = [permission]; + } + + if (existing.includes(permission)) { + await ExtensionPermissions.remove(otherId, permissions); + } else { + await ExtensionPermissions.add(otherId, permissions); + } + await change; + } + + if (optional_permissions.length) { + for (let name of optional_permissions) { + // Set of permissions represented by this key. + let perms = [name]; + if (name === optional_overlapping[0]) { + perms = optional_overlapping; + } + + // Check the row is a permission row with the correct key on the toggle + // control. + let row = permission_rows.shift(); + let toggle = row.querySelector("moz-toggle"); + let label = toggle.labelEl; + + let str = optional_strings[name]; + if (str) { + is(label.textContent.trim(), str, `Expected permission string ${str}`); + } + + ok( + row.classList.contains("permission-info"), + `optional permission row for ${name}` + ); + is( + toggle.getAttribute("permission-key"), + name, + `optional permission toggle exists for ${name}` + ); + + await testTogglePermissionButton(perms, toggle); + await testTogglePermissionButton(perms, toggle, true); + + for (let perm of perms) { + // make a change "outside" the UI and check the values. + // toggle twice to test both add/remove. + await testExternalPermissionChange(perm, toggle); + // change another addon to mess around with optional permission + // values to see if it effects the addon we're testing here. The + // next check would fail if anything bleeds onto other addons. + await testOtherPermissionChange(perm, toggle); + // repeat the "outside" test. + await testExternalPermissionChange(perm, toggle); + } + } + } + + if (!view) { + await closeView(win); + } +} + +async function testPermissionsView({ manifestV3enabled, manifest_version }) { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.manifestV3.enabled", manifestV3enabled]], + }); + + // pre-set a permission prior to starting extensions. + await ExtensionPermissions.add("addon4@mochi.test", { + permissions: ["tabs"], + origins: [], + }); + + let extensions = await getExtensions({ manifest_version }); + + info("Check add-on with required permissions"); + if (manifest_version < 3) { + await runTest({ + extension: extensions["addon1@mochi.test"], + permissions: ["<all_urls>", "tabs", "webNavigation"], + }); + } else { + await runTest({ + extension: extensions["addon1@mochi.test"], + permissions: ["tabs", "webNavigation"], + optional_permissions: ["<all_urls>"], + }); + } + + info("Check add-on without any displayable permissions"); + await runTest({ extension: extensions["addon0@mochi.test"] }); + + info("Check add-on with only one optional origin."); + await runTest({ + extension: extensions["addon2@mochi.test"], + optional_permissions: manifestV3enabled ? ["http://mochi.test/*"] : [], + optional_strings: { + "http://mochi.test/*": "Access your data for http://mochi.test", + }, + }); + + info("Check add-on with both required and optional permissions"); + await runTest({ + extension: extensions["addon3@mochi.test"], + permissions: ["tabs"], + optional_permissions: ["webNavigation", "<all_urls>"], + }); + + // Grant a specific optional host permission not listed in the manifest. + await ExtensionPermissions.add("addon3@mochi.test", { + permissions: [], + origins: ["https://example.com/*"], + }); + await extensions["addon3@mochi.test"].awaitMessage("permission-added"); + + info("Check addon3 again and expect the new optional host permission"); + await runTest({ + extension: extensions["addon3@mochi.test"], + permissions: ["tabs"], + optional_permissions: [ + "webNavigation", + "<all_urls>", + ...(manifestV3enabled ? ["https://example.com/*"] : []), + ], + optional_enabled: ["https://example.com/*"], + optional_strings: { + "https://example.com/*": "Access your data for https://example.com", + }, + }); + + info("Check add-on with only optional permissions, tabs is pre-enabled"); + await runTest({ + extension: extensions["addon4@mochi.test"], + optional_permissions: ["tabs", "webNavigation"], + optional_enabled: ["tabs"], + }); + + info("Check add-on with a global match pattern in place of all urls"); + await runTest({ + extension: extensions["addon5@mochi.test"], + optional_permissions: ["*://*/*"], + }); + + info("Check privileged add-on with non-web origin permissions"); + await runTest({ + extension: extensions["priv6@mochi.test"], + optional_permissions: [ + "<all_urls>", + ...(manifestV3enabled ? ["*://*.mozilla.com/*"] : []), + ], + optional_overlapping: ["<all_urls>", "*://*/*"], + optional_strings: { + "*://*.mozilla.com/*": + "Access your data for sites in the *://mozilla.com domain", + }, + }); + + info(`Check that <all_urls> is used over other "all websites" permissions`); + await runTest({ + extension: extensions["addon7@mochi.test"], + optional_permissions: ["<all_urls>"], + optional_overlapping: ["<all_urls>", "https://*/*"], + }); + + info(`Also check different "all sites" permissions in the manifest`); + await runTest({ + extension: extensions["addon8@mochi.test"], + optional_permissions: ["https://*/*"], + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + optional_overlapping: ["https://*/*", "http://*/*"], + }); + + for (let ext of Object.values(extensions)) { + await ext.unload(); + } + + await SpecialPowers.popPrefEnv(); +} + +add_task(async function testPermissionsView_MV2_manifestV3disabled() { + await testPermissionsView({ manifestV3enabled: false, manifest_version: 2 }); +}); + +add_task(async function testPermissionsView_MV2_manifestV3enabled() { + await testPermissionsView({ manifestV3enabled: true, manifest_version: 2 }); +}); + +add_task(async function testPermissionsView_MV3() { + await testPermissionsView({ manifestV3enabled: true, manifest_version: 3 }); +}); + +add_task(async function testPermissionsViewStates() { + let ID = "addon@mochi.test"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test add-on 3", + version: "1.0", + browser_specific_settings: { gecko: { id: ID } }, + permissions: ["tabs"], + optional_permissions: ["webNavigation", "<all_urls>"], + }, + useAddonManager: "permanent", + }); + await extension.startup(); + + info( + "Check toggling permissions on a disabled addon with addon3@mochi.test." + ); + let view = await loadInitialView("extension"); + let addon = await AddonManager.getAddonByID(ID); + await addon.disable(); + ok(addon.userDisabled, "addon is disabled"); + await runTest({ + extension, + permissions: ["tabs"], + optional_permissions: ["webNavigation", "<all_urls>"], + view, + }); + await addon.enable(); + ok(!addon.userDisabled, "addon is enabled"); + + async function install_addon(extensionData) { + let xpi = await AddonTestUtils.createTempWebExtensionFile(extensionData); + let { addon } = await AddonTestUtils.promiseInstallFile(xpi); + return addon; + } + + function wait_for_addon_item_updated(addonId) { + return BrowserTestUtils.waitForEvent(getAddonCard(view, addonId), "update"); + } + + let promiseItemUpdated = wait_for_addon_item_updated(ID); + addon = await install_addon({ + manifest: { + name: "Test add-on 3", + version: "2.0", + browser_specific_settings: { gecko: { id: ID } }, + optional_permissions: ["webNavigation"], + }, + useAddonManager: "permanent", + }); + is(addon.version, "2.0", "addon upgraded"); + await promiseItemUpdated; + + await runTest({ + addonId: addon.id, + optional_permissions: ["webNavigation"], + view, + }); + + // While the view is still available, test setting a permission + // that is not in the manifest of the addon. + let card = getAddonCard(view, addon.id); + await Assert.rejects( + card.setAddonPermission("webRequest", "permission", "add"), + /permission missing from manifest/, + "unable to set the addon permission" + ); + + await closeView(view); + await extension.unload(); +}); + +add_task(async function testAllUrlsNotGrantedUnconditionally_MV3() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.manifestV3.enabled", true]], + }); + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 3, + host_permissions: ["<all_urls>"], + }, + async background() { + const perms = await browser.permissions.getAll(); + browser.test.sendMessage("granted-permissions", perms); + }, + }); + + await extension.startup(); + const perms = await extension.awaitMessage("granted-permissions"); + ok( + !perms.origins.includes("<all_urls>"), + "Optional <all_urls> should not be granted as host permission yet" + ); + ok( + !perms.permissions.includes("<all_urls>"), + "Optional <all_urls> should not be granted as an API permission neither" + ); + + await extension.unload(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_OneOfMany_AllSites_toggle() { + // ESLint autofix will silently convert http://*/* match patterns into https. + /* eslint-disable @microsoft/sdl/no-insecure-url */ + let id = "addon9@mochi.test"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test add-on 9", + browser_specific_settings: { gecko: { id } }, + optional_permissions: ["http://*/*", "https://*/*"], + }, + background, + useAddonManager: "permanent", + }); + await extension.startup(); + + // Grant the second "all sites" permission as listed in the manifest. + await ExtensionPermissions.add("addon9@mochi.test", { + permissions: [], + origins: ["https://*/*"], + }); + await extension.awaitMessage("permission-added"); + + let view = await loadInitialView("extension"); + let addon = await AddonManager.getAddonByID(id); + + let card = getAddonCard(view, addon.id); + + let permsSection = card.querySelector("addon-permissions-list"); + if (!permsSection) { + ok(!card.hasAttribute("expanded"), "The list card is not expanded"); + let loaded = waitForViewLoad(view); + card.querySelector('[action="expand"]').click(); + await loaded; + } + + card = getAddonCard(view, addon.id); + let { deck, tabGroup } = card.details; + + let permsBtn = tabGroup.querySelector('[name="permissions"]'); + let permsShown = BrowserTestUtils.waitForEvent(deck, "view-changed"); + permsBtn.click(); + await permsShown; + + permsSection = card.querySelector("addon-permissions-list"); + let permission_rows = permsSection.querySelectorAll(".permission-info"); + is(permission_rows.length, 1, "Only one 'all sites' permission toggle."); + + let row = permission_rows[0]; + let toggle = row.querySelector("moz-toggle"); + ok( + row.classList.contains("permission-info"), + `optional permission row for "http://*/*"` + ); + is( + toggle.getAttribute("permission-key"), + "http://*/*", + `optional permission toggle exists for "http://*/*"` + ); + ok(toggle.pressed, "Expect 'all sites' toggle to be set."); + + // Revoke the second "all sites" permission, expect toggle to be unchecked. + await ExtensionPermissions.remove("addon9@mochi.test", { + permissions: [], + origins: ["https://*/*"], + }); + await extension.awaitMessage("permission-removed"); + ok(!toggle.pressed, "Expect 'all sites' toggle not to be pressed."); + + toggle.click(); + + let granted = await extension.awaitMessage("permission-added"); + Assert.deepEqual(granted, { + permissions: [], + origins: ["http://*/*", "https://*/*"], + }); + + await closeView(view); + await extension.unload(); + /* eslint-enable @microsoft/sdl/no-insecure-url */ +}); + +add_task(async function testOverrideLocalization() { + // Mock a fluent file. + const l10nReg = L10nRegistry.getInstance(); + const source = L10nFileSource.createMock( + "mock", + "app", + ["en-US"], + "/localization/", + [ + { + path: "/localization/mock.ftl", + source: ` +webext-perms-description-test-tabs = Custom description for the tabs permission +`, + }, + ] + ); + l10nReg.registerSources([source]); + + // Add the mocked fluent file to PERMISSION_L10N and override the tabs + // permission to use the alternative string. In a real world use-case, this + // would be used to add non-toolkit fluent files with permission strings of + // APIs which are defined outside of toolkit. + PERMISSION_L10N.addResourceIds(["mock.ftl"]); + PERMISSION_L10N_ID_OVERRIDES.set( + "tabs", + "webext-perms-description-test-tabs" + ); + + let mockCleanup = () => { + // Make sure cleanup is executed only once. + mockCleanup = () => {}; + + // Remove the non-toolkit permission string. + PERMISSION_L10N.removeResourceIds(["mock.ftl"]); + PERMISSION_L10N_ID_OVERRIDES.delete("tabs"); + l10nReg.removeSources(["mock"]); + }; + registerCleanupFunction(mockCleanup); + + // Load an example add-on which uses the tabs permission. + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 2, + name: "Simple test add-on", + browser_specific_settings: { gecko: { id: "testAddon@mochi.test" } }, + permissions: ["tabs"], + }, + background, + useAddonManager: "temporary", + }); + await extension.startup(); + let addonId = extension.id; + + let win = await loadInitialView("extension"); + + // Open the card and navigate to its permission list. + let card = getAddonCard(win, addonId); + let permsSection = card.querySelector("addon-permissions-list"); + if (!permsSection) { + ok(!card.hasAttribute("expanded"), "The list card is not expanded"); + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + } + + card = getAddonCard(win, addonId); + let { deck, tabGroup } = card.details; + + let permsBtn = tabGroup.querySelector('[name="permissions"]'); + let permsShown = BrowserTestUtils.waitForEvent(deck, "view-changed"); + permsBtn.click(); + await permsShown; + let permissionList = card.querySelector("addon-permissions-list"); + let permissionEntries = Array.from(permissionList.querySelectorAll("li")); + Assert.equal( + permissionEntries.length, + 1, + "Should find a single permission entry" + ); + Assert.equal( + permissionEntries[0].textContent, + "Custom description for the tabs permission", + "Should find the non-default permission description" + ); + + await closeView(win); + await extension.unload(); + + mockCleanup(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js b/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js new file mode 100644 index 0000000000..76e7f2b255 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js @@ -0,0 +1,1675 @@ +/* eslint max-len: ["error", 80] */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const { ExtensionPermissions } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionPermissions.sys.mjs" +); + +const { QuarantinedDomains } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionPermissions.sys.mjs" +); + +const SUPPORT_URL = Services.urlFormatter.formatURL( + Services.prefs.getStringPref("app.support.baseURL") +); +const PB_SUMO_URL = SUPPORT_URL + "extensions-pb"; +const DEFAULT_THEME_ID = "default-theme@mozilla.org"; +const DARK_THEME_ID = "firefox-compact-dark@mozilla.org"; + +let gProvider; +let promptService; + +AddonTestUtils.initMochitest(this); + +function getDetailRows(card) { + return Array.from( + card.querySelectorAll('[name="details"] .addon-detail-row:not([hidden])') + ); +} + +async function checkLabel(row, name) { + let id; + if (name == "private-browsing") { + // This id is carried over from the old about:addons. + id = "detail-private-browsing-label"; + } else { + id = `addon-detail-${name}-label`; + } + const doc = row.ownerDocument; + await doc.l10n.translateElements([row]); + const rowHeaderEl = row.firstElementChild; + is(doc.l10n.getAttributes(rowHeaderEl).id, id, `The ${name} label is set`); + if (row.getAttribute("role") === "group") { + // For the rows on which the role="group" attribute is set, + // let's make sure that the element itself includes an aria-label + // which provides to the screen reader a label similar to the one + // rendered as the visual section header. + // + // NOTE: more screen reader accessibility assertions are being + // covered by the checkRowScreenReaderAccessibility test helper. + is( + row.getAttribute("aria-label"), + rowHeaderEl.textContent, + "expect an aria-label from role=group row to match row header el text" + ); + // For these rows we expect rowHeaderEl to be a span. + is(rowHeaderEl.tagName, "SPAN", "row header element should be a span"); + } else { + // For the other rows which we have not set a role="group" attribute + // on, we expect the rowHeaderEl to still be a label. + is( + rowHeaderEl.tagName, + "LABEL", + "row header element expected to be a label" + ); + } +} + +async function checkRowScreenReaderAccessibility( + row, + { groupName, expectedFluentId } +) { + const doc = row.ownerDocument; + // Make sure the row isn't missing any strings expected to be associated + // to the fluent ids (which would make translateElements to reject + // and the test to fail explicitly). + await doc.l10n.translateElements([row]); + is( + row.getAttribute("role"), + "group", + `Expect ${groupName} row to have role group` + ); + is( + doc.l10n.getAttributes(row).id, + expectedFluentId, + `Got expected fluent id associated to the ${groupName} row` + ); + // Make sure that screen readers will be able to announce to the + // user what is the group of controls being entered. + ok( + !!row.getAttribute("aria-label")?.length, + `Expect non empty aria-label on the ${groupName} row` + ); +} + +async function checkQuarantinedDomainsUserAllowedRows(card, rows) { + // Account for the rows related to per-addon quarantineIgnoredByUser UI, + // underling functionality of the UI is checked in its own test task. + const doc = card.ownerDocument; + if (card.addon.canChangeQuarantineIgnored) { + let row = rows.shift(); + await checkLabel(row, "quarantined-domains"); + await checkRowScreenReaderAccessibility(row, { + groupName: "quarantined domains exempt controls", + expectedFluentId: "addon-detail-group-label-quarantined-domains", + }); + + // quarantineIgnoredByUser UI help text. + row = rows.shift(); + ok(row.classList.contains("addon-detail-help-row"), "There's a help row"); + ok(!row.hidden, "The help row is shown"); + is( + doc.l10n.getAttributes(row.firstElementChild).id, + "addon-detail-quarantined-domains-help", + "The help row is for quarantined domains" + ); + } +} + +function formatUrl(contentAttribute, url) { + let parsedUrl = new URL(url); + parsedUrl.searchParams.set("utm_source", "firefox-browser"); + parsedUrl.searchParams.set("utm_medium", "firefox-browser"); + parsedUrl.searchParams.set("utm_content", contentAttribute); + return parsedUrl.href; +} + +function checkLink(link, url, text = url) { + ok(link, "There is a link"); + is(link.href, url, "The link goes to the URL"); + if (text instanceof Object) { + // Check the fluent data. + Assert.deepEqual( + link.ownerDocument.l10n.getAttributes(link), + text, + "The fluent data is set correctly" + ); + } else { + // Just check text. + is(link.textContent, text, "The text is set"); + } + is(link.getAttribute("target"), "_blank", "The link opens in a new tab"); +} + +function checkOptions(doc, options, expectedOptions) { + let numOptions = expectedOptions.length; + is(options.length, numOptions, `There are ${numOptions} options`); + for (let i = 0; i < numOptions; i++) { + let option = options[i]; + is(option.children.length, 2, "There are 2 children for the option"); + let input = option.firstElementChild; + is(input.tagName, "INPUT", "The input is first"); + let text = option.lastElementChild; + is(text.tagName, "SPAN", "The label text is second"); + let expected = expectedOptions[i]; + is(input.value, expected.value, "The value is right"); + is(input.checked, expected.checked, "The checked property is correct"); + Assert.deepEqual( + doc.l10n.getAttributes(text), + { id: expected.label, args: null }, + "The label has the right text" + ); + } +} + +function assertDeckHeadingHidden(group) { + ok(group.hidden, "The tab group is hidden"); + let buttons = group.querySelectorAll(".tab-button"); + for (let button of buttons) { + Assert.equal(button.offsetHeight, 0, `The ${button.name} is hidden`); + } +} + +function assertDeckHeadingButtons(group, visibleButtons) { + ok(!group.hidden, "The tab group is shown"); + let buttons = group.querySelectorAll(".tab-button"); + Assert.greaterOrEqual( + buttons.length, + visibleButtons.length, + `There should be at least ${visibleButtons.length} buttons` + ); + for (let button of buttons) { + if (visibleButtons.includes(button.name)) { + ok(!button.hidden, `The ${button.name} is shown`); + } else { + ok(button.hidden, `The ${button.name} is hidden`); + } + } +} + +async function hasPrivateAllowed(id) { + let perms = await ExtensionPermissions.get(id); + return perms.permissions.includes("internal:privateBrowsingAllowed"); +} + +async function assertBackButtonIsDisabled(win) { + let backButton = await BrowserTestUtils.waitForCondition(async () => { + let backButton = win.document.querySelector(".back-button"); + + // Wait until the button is visible in the page. + return backButton && !backButton.hidden ? backButton : false; + }); + + ok(backButton, "back button is rendered"); + ok(backButton.disabled, "back button is disabled"); +} + +add_setup(async function enableHtmlViews() { + gProvider = new MockProvider(["extension", "sitepermission"]); + gProvider.createAddons([ + { + id: "addon1@mochi.test", + name: "Test add-on 1", + creator: { name: "The creator", url: "http://addons.mozilla.org/me" }, + version: "3.1", + description: "Short description", + fullDescription: "Longer description\nWith brs!", + type: "extension", + contributionURL: "http://example.com/contribute", + averageRating: 4.279, + userPermissions: { + origins: ["<all_urls>", "file://*/*"], + permissions: ["alarms", "contextMenus", "tabs", "webNavigation"], + }, + reviewCount: 5, + reviewURL: "http://addons.mozilla.org/reviews", + homepageURL: "http://example.com/addon1", + updateDate: new Date("2019-03-07T01:00:00"), + applyBackgroundUpdates: AddonManager.AUTOUPDATE_ENABLE, + }, + { + id: "addon2@mochi.test", + name: "Test add-on 2", + creator: { name: "I made it" }, + description: "Short description", + userPermissions: { + origins: [], + permissions: ["alarms", "contextMenus"], + }, + type: "extension", + }, + { + id: "addon3@mochi.test", + name: "Test add-on 3", + creator: { name: "Look a super long description" }, + description: "Short description", + fullDescription: "Mozilla\n".repeat(100), + userPermissions: { + origins: [], + permissions: ["alarms", "contextMenus"], + }, + type: "extension", + contributionURL: "http://example.com/contribute", + updateDate: new Date("2022-03-07T01:00:00"), + }, + { + id: "addon4@mochi.test", + name: "Test add-on 4", + creator: { name: "Some name" }, + description: "Short description", + userPermissions: { + origins: [], + permissions: ["alarms", "contextMenus"], + }, + type: "extension", + reviewCount: 0, + reviewURL: "http://addons.mozilla.org/reviews", + averageRating: 0, + }, + { + // NOTE: Keep the mock properties in sync with the one that + // SitePermsAddonWrapper would be providing in real synthetic + // addon entries managed by the SitePermsAddonProvider. + id: "sitepermission@mochi.test", + version: "2.0", + name: "Test site permission add-on", + description: "permission description", + fullDescription: "detailed description", + siteOrigin: "http://mochi.test", + sitePermissions: ["midi"], + type: "sitepermission", + permissions: AddonManager.PERM_CAN_UNINSTALL, + }, + { + id: "theme1@mochi.test", + name: "Test theme", + creator: { name: "Artist", url: "http://example.com/artist" }, + description: "A nice tree", + type: "theme", + screenshots: [ + { + url: "http://example.com/preview-wide.png", + width: 760, + height: 92, + }, + { + url: "http://example.com/preview.png", + width: 680, + height: 92, + }, + ], + }, + ]); + + promptService = mockPromptService(); +}); + +add_task(async function testOpenDetailView() { + let id = "test@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test", + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "temporary", + }); + let id2 = "test2@mochi.test"; + let extension2 = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test", + browser_specific_settings: { gecko: { id: id2 } }, + }, + useAddonManager: "temporary", + }); + + await extension.startup(); + await extension2.startup(); + + const goBack = async win => { + let loaded = waitForViewLoad(win); + let backButton = win.document.querySelector(".back-button"); + ok(!backButton.disabled, "back button is enabled"); + backButton.click(); + await loaded; + }; + + let win = await loadInitialView("extension"); + + // Test click on card to open details. + let card = getAddonCard(win, id); + ok(!card.querySelector("addon-details"), "The card doesn't have details"); + let loaded = waitForViewLoad(win); + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a non-interactive container to open the card + // with a mouse, while its inner link element is accessible and is being + // tested in other test cases, thus this rule check shall be ignored by + // a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + EventUtils.synthesizeMouseAtCenter(card, { clickCount: 1 }, win); + AccessibilityUtils.resetEnv(); + await loaded; + + card = getAddonCard(win, id); + ok(card.querySelector("addon-details"), "The card now has details"); + + await goBack(win); + + // Test using more options menu. + card = getAddonCard(win, id); + loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + card = getAddonCard(win, id); + ok(card.querySelector("addon-details"), "The card now has details"); + + await goBack(win); + + card = getAddonCard(win, id2); + loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + await goBack(win); + + // Test click on add-on name. + card = getAddonCard(win, id2); + ok(!card.querySelector("addon-details"), "The card isn't expanded"); + let addonName = card.querySelector(".addon-name"); + loaded = waitForViewLoad(win); + EventUtils.synthesizeMouseAtCenter(addonName, {}, win); + await loaded; + card = getAddonCard(win, id2); + ok(card.querySelector("addon-details"), "The card is expanded"); + + await closeView(win); + await extension.unload(); + await extension2.unload(); +}); + +add_task(async function testDetailOperations() { + let id = "test@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test", + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "temporary", + }); + + await extension.startup(); + + let win = await loadInitialView("extension"); + let doc = win.document; + + let card = getAddonCard(win, id); + ok(!card.querySelector("addon-details"), "The card doesn't have details"); + let loaded = waitForViewLoad(win); + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a non-interactive container to open the card + // with a mouse, while its inner link element is accessible and is being + // tested in other test cases, thus this rule check shall be ignored by + // a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + EventUtils.synthesizeMouseAtCenter(card, { clickCount: 1 }, win); + AccessibilityUtils.resetEnv(); + await loaded; + + card = getAddonCard(win, id); + let panel = card.querySelector("panel-list"); + + // Check button visibility. + let disableButton = card.querySelector('[action="toggle-disabled"]'); + ok(!disableButton.hidden, "The disable button is visible"); + + let removeButton = panel.querySelector('[action="remove"]'); + ok(!removeButton.hidden, "The remove button is visible"); + + let separator = panel.querySelector("hr:last-of-type"); + ok(separator.hidden, "The separator is hidden"); + + let expandButton = panel.querySelector('[action="expand"]'); + ok(expandButton.hidden, "The expand button is hidden"); + + // Check toggling disabled. + let name = card.addonNameEl; + is(name.textContent, "Test", "The name is set when enabled"); + is(doc.l10n.getAttributes(name).id, null, "There is no l10n name"); + + // Disable the extension. + let disableToggled = BrowserTestUtils.waitForEvent(card, "update"); + disableButton.click(); + await disableToggled; + + // The (disabled) text should be shown now. + Assert.deepEqual( + doc.l10n.getAttributes(name), + { id: "addon-name-disabled", args: { name: "Test" } }, + "The name is updated to the disabled text" + ); + + // Enable the add-on. + let extensionStarted = AddonTestUtils.promiseWebExtensionStartup(id); + disableToggled = BrowserTestUtils.waitForEvent(card, "update"); + disableButton.click(); + await Promise.all([disableToggled, extensionStarted]); + + // Name is just the add-on name again. + is(name.textContent, "Test", "The name is reset when enabled"); + is(doc.l10n.getAttributes(name).id, null, "There is no l10n name"); + + // Remove but cancel. + let cancelled = BrowserTestUtils.waitForEvent(card, "remove-cancelled"); + removeButton.click(); + await cancelled; + + // Remove the extension. + let viewChanged = waitForViewLoad(win); + // Tell the mock prompt service that the prompt was accepted. + promptService._response = 0; + removeButton.click(); + await viewChanged; + + // We're on the list view now and there's no card for this extension. + const addonList = doc.querySelector("addon-list"); + ok(addonList, "There's an addon-list now"); + ok(!getAddonCard(win, id), "The extension no longer has a card"); + let addon = await AddonManager.getAddonByID(id); + ok( + addon && !!(addon.pendingOperations & AddonManager.PENDING_UNINSTALL), + "The addon is pending uninstall" + ); + + // Ensure that a pending uninstall bar has been created for the + // pending uninstall extension, and pressing the undo button will + // refresh the list and render a card to the re-enabled extension. + assertHasPendingUninstalls(addonList, 1); + assertHasPendingUninstallAddon(addonList, addon); + + extensionStarted = AddonTestUtils.promiseWebExtensionStartup(addon.id); + await testUndoPendingUninstall(addonList, addon); + info("Wait for the pending uninstall addon complete restart"); + await extensionStarted; + + card = getAddonCard(win, addon.id); + ok(card, "Addon card rendered after clicking pending uninstall undo button"); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testFullDetails() { + let id = "addon1@mochi.test"; + let headingId = "addon1_mochi_test-heading"; + let win = await loadInitialView("extension"); + let doc = win.document; + + // The list card. + let card = getAddonCard(win, id); + ok(!card.hasAttribute("expanded"), "The list card is not expanded"); + + // Make sure the preview is hidden. + let preview = card.querySelector(".card-heading-image"); + ok(preview, "There is a preview"); + is(preview.hidden, true, "The preview is hidden"); + + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + // This is now the detail card. + card = getAddonCard(win, id); + ok(card.hasAttribute("expanded"), "The detail card is expanded"); + + let cardHeading = card.querySelector("h1"); + is(cardHeading.textContent, "Test add-on 1", "Card heading is set"); + is(cardHeading.id, headingId, "Heading has correct id"); + is( + card.querySelector(".card").getAttribute("aria-labelledby"), + headingId, + "Card is labelled by the heading" + ); + + // Make sure the preview is hidden. + preview = card.querySelector(".card-heading-image"); + ok(preview, "There is a preview"); + is(preview.hidden, true, "The preview is hidden"); + + let details = card.querySelector("addon-details"); + + // Check all the deck buttons are hidden. + assertDeckHeadingButtons(details.tabGroup, ["details", "permissions"]); + + let desc = details.querySelector(".addon-detail-description"); + is( + desc.innerHTML, + "Longer description<br>With brs!", + "The full description replaces newlines with <br>" + ); + + let sitepermissionsRow = details.querySelector( + ".addon-detail-sitepermissions" + ); + is( + sitepermissionsRow.hidden, + true, + "AddonSitePermissionsList should be hidden for this addon type" + ); + + // Check the show more button is not there + const showMoreBtn = card.querySelector(".addon-detail-description-toggle"); + ok(showMoreBtn.hidden, "The show more button is not visible"); + + let contrib = details.querySelector(".addon-detail-contribute"); + ok(contrib, "The contribution section is visible"); + + let waitForTab = BrowserTestUtils.waitForNewTab( + gBrowser, + "http://example.com/contribute" + ); + contrib.querySelector("button").click(); + BrowserTestUtils.removeTab(await waitForTab); + + let rows = getDetailRows(card); + + // Auto updates. + let row = rows.shift(); + + await checkLabel(row, "updates"); + await checkRowScreenReaderAccessibility(row, { + groupName: "updates controls", + expectedFluentId: "addon-detail-group-label-updates", + }); + + let expectedOptions = [ + { value: "1", label: "addon-detail-updates-radio-default", checked: false }, + { value: "2", label: "addon-detail-updates-radio-on", checked: true }, + { value: "0", label: "addon-detail-updates-radio-off", checked: false }, + ]; + let options = row.lastElementChild.querySelectorAll("label"); + checkOptions(doc, options, expectedOptions); + + // Private browsing, functionality checked in another test. + row = rows.shift(); + await checkLabel(row, "private-browsing"); + await checkRowScreenReaderAccessibility(row, { + groupName: "private browsing controls", + expectedFluentId: "addon-detail-group-label-private-browsing", + }); + + // Private browsing help text. + row = rows.shift(); + ok(row.classList.contains("addon-detail-help-row"), "There's a help row"); + ok(!row.hidden, "The help row is shown"); + is( + doc.l10n.getAttributes(row).id, + "addon-detail-private-browsing-help", + "The help row is for private browsing" + ); + + await checkQuarantinedDomainsUserAllowedRows(card, rows); + + // Author. + row = rows.shift(); + await checkLabel(row, "author"); + let link = row.querySelector("a"); + let authorLink = formatUrl( + "addons-manager-user-profile-link", + "http://addons.mozilla.org/me" + ); + checkLink(link, authorLink, "The creator"); + + // Version. + row = rows.shift(); + await checkLabel(row, "version"); + let text = row.lastChild; + is(text.textContent, "3.1", "The version is set"); + + // Last updated. + row = rows.shift(); + await checkLabel(row, "last-updated"); + text = row.lastChild; + is(text.textContent, "March 7, 2019", "The last updated date is set"); + + // Homepage. + row = rows.shift(); + await checkLabel(row, "homepage"); + link = row.querySelector("a"); + checkLink(link, "http://example.com/addon1"); + + // Reviews. + row = rows.shift(); + await checkLabel(row, "rating"); + let rating = row.lastElementChild; + ok(rating.classList.contains("addon-detail-rating"), "Found the rating el"); + let mozFiveStar = rating.querySelector("moz-five-star"); + is(mozFiveStar.rating, 4.279, "Exact rating used for calculations"); + let stars = Array.from(mozFiveStar.starEls); + let fullAttrs = stars.map(star => star.getAttribute("fill")).join(","); + is(fullAttrs, "full,full,full,full,half", "Four and a half stars are full"); + link = rating.querySelector("a"); + let reviewsLink = formatUrl( + "addons-manager-reviews-link", + "http://addons.mozilla.org/reviews" + ); + checkLink(link, reviewsLink, { + id: "addon-detail-reviews-link", + args: { numberOfReviews: 5 }, + }); + + // While we are here, let's test edge cases of star ratings. + async function testRating(rating, ratingRounded, expectation) { + mozFiveStar.rating = rating; + await mozFiveStar.updateComplete; + if (mozFiveStar.ownerDocument.hasPendingL10nMutations) { + await BrowserTestUtils.waitForEvent( + mozFiveStar.ownerDocument, + "L10nMutationsFinished" + ); + } + let starsString = Array.from(mozFiveStar.starEls) + .map(star => star.getAttribute("fill")) + .join(","); + is(starsString, expectation, `Rendering of rating ${rating}`); + + is( + mozFiveStar.starsWrapperEl.title, + `Rated ${ratingRounded} out of 5`, + "Rendered title must contain at most one fractional digit" + ); + } + await testRating(0.0, "0", "empty,empty,empty,empty,empty"); + await testRating(0.123, "0.1", "empty,empty,empty,empty,empty"); + await testRating(0.249, "0.2", "empty,empty,empty,empty,empty"); + await testRating(0.25, "0.3", "half,empty,empty,empty,empty"); + await testRating(0.749, "0.7", "half,empty,empty,empty,empty"); + await testRating(0.75, "0.8", "full,empty,empty,empty,empty"); + await testRating(1.0, "1", "full,empty,empty,empty,empty"); + await testRating(4.249, "4.2", "full,full,full,full,empty"); + await testRating(4.25, "4.3", "full,full,full,full,half"); + await testRating(4.749, "4.7", "full,full,full,full,half"); + await testRating(5.0, "5", "full,full,full,full,full"); + + // That should've been all the rows. + is(rows.length, 0, "There are no more rows left"); + + await closeView(win); +}); + +add_task(async function testFullDetailsShowMoreButton() { + const id = "addon3@mochi.test"; + const win = await loadInitialView("extension"); + + // The list card. + let card = getAddonCard(win, id); + const loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + // This is now the detail card. + card = getAddonCard(win, id); + + // Check the show more button is there + const showMoreBtn = card.querySelector(".addon-detail-description-toggle"); + ok(!showMoreBtn.hidden, "The show more button is visible"); + + const descriptionWrapper = card.querySelector( + ".addon-detail-description-wrapper" + ); + ok( + descriptionWrapper.classList.contains("addon-detail-description-collapse"), + "The long description is collapsed" + ); + + // After click the description should be expanded + showMoreBtn.click(); + ok( + !descriptionWrapper.classList.contains("addon-detail-description-collapse"), + "The long description is expanded" + ); + + await closeView(win); +}); + +add_task(async function testMinimalExtension() { + let win = await loadInitialView("extension"); + let doc = win.document; + + let card = getAddonCard(win, "addon2@mochi.test"); + ok(!card.hasAttribute("expanded"), "The list card is not expanded"); + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + card = getAddonCard(win, "addon2@mochi.test"); + let details = card.querySelector("addon-details"); + + // Check all the deck buttons are hidden. + assertDeckHeadingButtons(details.tabGroup, ["details", "permissions"]); + + let desc = details.querySelector(".addon-detail-description"); + is(desc.textContent, "", "There is no full description"); + + let contrib = details.querySelector(".addon-detail-contribute"); + ok(contrib.hidden, "The contribution element is hidden"); + + let rows = getDetailRows(card); + + // Automatic updates. + let row = rows.shift(); + await checkLabel(row, "updates"); + + // Private browsing settings. + row = rows.shift(); + await checkLabel(row, "private-browsing"); + + // Private browsing help text. + row = rows.shift(); + ok(row.classList.contains("addon-detail-help-row"), "There's a help row"); + ok(!row.hidden, "The help row is shown"); + is( + doc.l10n.getAttributes(row).id, + "addon-detail-private-browsing-help", + "The help row is for private browsing" + ); + + await checkQuarantinedDomainsUserAllowedRows(card, rows); + + // Author. + row = rows.shift(); + await checkLabel(row, "author"); + let text = row.lastChild; + is(text.textContent, "I made it", "The author is set"); + ok(Text.isInstance(text), "The author is a text node"); + + is(rows.length, 0, "There are no more rows"); + + await closeView(win); +}); + +add_task(async function testDefaultTheme() { + let win = await loadInitialView("theme"); + + // The list card. + let card = getAddonCard(win, DEFAULT_THEME_ID); + ok(!card.hasAttribute("expanded"), "The list card is not expanded"); + + let preview = card.querySelector(".card-heading-image"); + ok(preview, "There is a preview"); + ok(!preview.hidden, "The preview is visible"); + + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + card = getAddonCard(win, DEFAULT_THEME_ID); + + preview = card.querySelector(".card-heading-image"); + ok(preview, "There is a preview"); + ok(!preview.hidden, "The preview is visible"); + + // Check all the deck buttons are hidden. + assertDeckHeadingHidden(card.details.tabGroup); + + let rows = getDetailRows(card); + + // Author. + let author = rows.shift(); + await checkLabel(author, "author"); + let text = author.lastChild; + is(text.textContent, "Mozilla", "The author is set"); + + // Version. + let version = rows.shift(); + await checkLabel(version, "version"); + is(version.lastChild.textContent, "1.3", "It's always version 1.3"); + + // Last updated. + let lastUpdated = rows.shift(); + await checkLabel(lastUpdated, "last-updated"); + let dateText = lastUpdated.lastChild.textContent; + ok(dateText, "There is a date set"); + ok(!dateText.includes("Invalid Date"), `"${dateText}" should be a date`); + + is(rows.length, 0, "There are no more rows"); + + await closeView(win); +}); + +add_task(async function testStaticTheme() { + let win = await loadInitialView("theme"); + + // The list card. + let card = getAddonCard(win, "theme1@mochi.test"); + ok(!card.hasAttribute("expanded"), "The list card is not expanded"); + + // Make sure the preview is set. + let preview = card.querySelector(".card-heading-image"); + ok(preview, "There is a preview"); + is(preview.src, "http://example.com/preview.png", "The preview URL is set"); + is(preview.width, 664, "The width is set"); + is(preview.height, 90, "The height is set"); + is(preview.hidden, false, "The preview is visible"); + + // Load the detail view. + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + card = getAddonCard(win, "theme1@mochi.test"); + + // Make sure the preview is still set. + preview = card.querySelector(".card-heading-image"); + ok(preview, "There is a preview"); + is(preview.src, "http://example.com/preview.png", "The preview URL is set"); + is(preview.width, 664, "The width is set"); + is(preview.height, 90, "The height is set"); + is(preview.hidden, false, "The preview is visible"); + + // Check all the deck buttons are hidden. + assertDeckHeadingHidden(card.details.tabGroup); + + let rows = getDetailRows(card); + + // Automatic updates. + let row = rows.shift(); + await checkLabel(row, "updates"); + + // Author. + let author = rows.shift(); + await checkLabel(author, "author"); + let text = author.lastElementChild; + is(text.textContent, "Artist", "The author is set"); + + is(rows.length, 0, "There was only 1 row"); + + await closeView(win); +}); + +add_task(async function testSitePermission() { + let win = await loadInitialView("sitepermission"); + + // The list card. + let card = getAddonCard(win, "sitepermission@mochi.test"); + ok(!card.hasAttribute("expanded"), "The list card is not expanded"); + + // Load the detail view. + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + card = getAddonCard(win, "sitepermission@mochi.test"); + + // Check all the deck buttons are hidden. + assertDeckHeadingHidden(card.details.tabGroup); + + let sitepermissionsRow = card.querySelector(".addon-detail-sitepermissions"); + is( + BrowserTestUtils.isVisible(sitepermissionsRow), + true, + "AddonSitePermissionsList should be visible for this addon type" + ); + + let [versionRow, ...restRows] = getDetailRows(card); + await checkLabel(versionRow, "version"); + + Assert.deepEqual( + restRows.map(row => row.getAttribute("class")), + [], + "All other details row are hidden as expected" + ); + + let permissions = Array.from( + card.querySelectorAll(".addon-permissions-list .permission-info") + ); + is(permissions.length, 1, "a permission is listed"); + is(permissions[0].textContent, "Access MIDI devices", "got midi permission"); + + await closeView(win); +}); + +add_task(async function testPrivateBrowsingExtension() { + let id = "pb@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "My PB extension", + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "permanent", + }); + + await extension.startup(); + + let win = await loadInitialView("extension"); + let doc = win.document; + + // The add-on shouldn't show that it's allowed yet. + let card = getAddonCard(win, id); + let badge = card.querySelector(".addon-badge-private-browsing-allowed"); + ok(badge.hidden, "The PB badge is hidden initially"); + ok(!(await hasPrivateAllowed(id)), "PB is not allowed"); + + // Load the detail view. + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + // The badge is still hidden on the detail view. + card = getAddonCard(win, id); + badge = card.querySelector(".addon-badge-private-browsing-allowed"); + ok(badge.hidden, "The PB badge is hidden on the detail view"); + ok(!(await hasPrivateAllowed(id)), "PB is not allowed"); + + let pbRow = card.querySelector(".addon-detail-row-private-browsing"); + let name = card.querySelector(".addon-name"); + + // Allow private browsing. + let [allow, disallow] = pbRow.querySelectorAll("input"); + let updated = BrowserTestUtils.waitForEvent(card, "update"); + + // Check that the disabled state isn't shown while reloading the add-on. + let addonDisabled = AddonTestUtils.promiseAddonEvent("onDisabled"); + allow.click(); + await addonDisabled; + is( + doc.l10n.getAttributes(name).id, + null, + "The disabled message is not shown for the add-on" + ); + + // Check the PB stuff. + await updated; + + // Not sure what better to await here. + await TestUtils.waitForCondition(() => !badge.hidden); + + ok(!badge.hidden, "The PB badge is now shown"); + ok(await hasPrivateAllowed(id), "PB is allowed"); + is( + doc.l10n.getAttributes(name).id, + null, + "The disabled message is not shown for the add-on" + ); + + info("Verify the badge links to the support page"); + let tabOpened = BrowserTestUtils.waitForNewTab(gBrowser, PB_SUMO_URL); + EventUtils.synthesizeMouseAtCenter(badge, {}, win); + let tab = await tabOpened; + BrowserTestUtils.removeTab(tab); + + // Disable the add-on and change the value. + updated = BrowserTestUtils.waitForEvent(card, "update"); + card.querySelector('[action="toggle-disabled"]').click(); + await updated; + + // It's still allowed in PB. + ok(await hasPrivateAllowed(id), "PB is allowed"); + ok(!badge.hidden, "The PB badge is shown"); + + // Disallow PB. + updated = BrowserTestUtils.waitForEvent(card, "update"); + disallow.click(); + await updated; + + ok(badge.hidden, "The PB badge is hidden"); + ok(!(await hasPrivateAllowed(id)), "PB is disallowed"); + + // Allow PB. + updated = BrowserTestUtils.waitForEvent(card, "update"); + allow.click(); + await updated; + + ok(!badge.hidden, "The PB badge is hidden"); + ok(await hasPrivateAllowed(id), "PB is disallowed"); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testInvalidExtension() { + let win = await open_manager("addons://detail/foo"); + let categoryUtils = new CategoryUtilities(win); + is( + categoryUtils.selectedCategory, + "discover", + "Should fall back to the discovery pane" + ); + + ok(!gBrowser.canGoBack, "The view has been replaced"); + + await close_manager(win); +}); + +add_task(async function testInvalidExtensionNoDiscover() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.getAddons.showPane", false]], + }); + + let win = await open_manager("addons://detail/foo"); + let categoryUtils = new CategoryUtilities(win); + is( + categoryUtils.selectedCategory, + "extension", + "Should fall back to the extension list if discover is disabled" + ); + + ok(!gBrowser.canGoBack, "The view has been replaced"); + + await close_manager(win); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function testExternalUninstall() { + let id = "remove@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Remove me", + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + let addon = await AddonManager.getAddonByID(id); + + let win = await loadInitialView("extension"); + let doc = win.document; + + // Load the detail view. + let card = doc.querySelector(`addon-card[addon-id="${id}"]`); + let detailsLoaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await detailsLoaded; + + // Uninstall the add-on with undo. Should go to extension list. + let listLoaded = waitForViewLoad(win); + await addon.uninstall(true); + await listLoaded; + + // Verify the list view was loaded and the card is gone. + let list = doc.querySelector("addon-list"); + ok(list, "Moved to a list page"); + is(list.type, "extension", "We're on the extension list page"); + card = list.querySelector(`addon-card[addon-id="${id}"]`); + ok(!card, "The card has been removed"); + + await extension.unload(); + closeView(win); +}); + +add_task(async function testExternalThemeUninstall() { + let id = "remove-theme@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id } }, + name: "Remove theme", + theme: {}, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + let addon = await AddonManager.getAddonByID(id); + + let win = await loadInitialView("theme"); + let doc = win.document; + + // Load the detail view. + let card = doc.querySelector(`addon-card[addon-id="${id}"]`); + let detailsLoaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await detailsLoaded; + + // Uninstall the add-on without undo. Should go to theme list. + let listLoaded = waitForViewLoad(win); + await addon.uninstall(); + await listLoaded; + + // Verify the list view was loaded and the card is gone. + let list = doc.querySelector("addon-list"); + ok(list, "Moved to a list page"); + is(list.type, "theme", "We're on the theme list page"); + card = list.querySelector(`addon-card[addon-id="${id}"]`); + ok(!card, "The card has been removed"); + + await extension.unload(); + closeView(win); +}); + +add_task(async function testPrivateBrowsingAllowedListView() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Allowed PB extension", + browser_specific_settings: { gecko: { id: "allowed@mochi.test" } }, + }, + useAddonManager: "permanent", + }); + + await extension.startup(); + let perms = { permissions: ["internal:privateBrowsingAllowed"], origins: [] }; + await ExtensionPermissions.add("allowed@mochi.test", perms); + let addon = await AddonManager.getAddonByID("allowed@mochi.test"); + await addon.reload(); + + let win = await loadInitialView("extension"); + + // The allowed extension should have a badge on load. + let card = getAddonCard(win, "allowed@mochi.test"); + let badge = card.querySelector(".addon-badge-private-browsing-allowed"); + ok(!badge.hidden, "The PB badge is shown for the allowed add-on"); + + await extension.unload(); + await closeView(win); +}); + +// When the back button is used, its disabled state will be updated. If it +// isn't updated when showing a view, then it will be disabled on the next +// use (bug 1551213) if the last use caused it to become disabled. +add_task(async function testGoBackButton() { + // Make sure the list view is the first loaded view so you cannot go back. + Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, "addons://list/extension"); + + let id = "addon1@mochi.test"; + let win = await loadInitialView("extension"); + let doc = win.document; + let backButton = doc.querySelector(".back-button"); + + let loadDetailView = () => { + let loaded = waitForViewLoad(win); + getAddonCard(win, id).querySelector("[action=expand]").click(); + return loaded; + }; + + let checkBackButtonState = () => { + is_element_visible(backButton, "Back button is visible on the detail page"); + ok(!backButton.disabled, "Back button is enabled"); + }; + + // Load the detail view, first time should be fine. + await loadDetailView(); + checkBackButtonState(); + + // Use the back button directly to pop from history and trigger its disabled + // state to be updated. + let loaded = waitForViewLoad(win); + backButton.click(); + await loaded; + + await loadDetailView(); + checkBackButtonState(); + + await closeView(win); +}); + +add_task(async function testEmptyMoreOptionsMenu() { + let theme = await AddonManager.getAddonByID(DEFAULT_THEME_ID); + ok(theme.isActive, "The default theme is enabled"); + + let win = await loadInitialView("theme"); + + let card = getAddonCard(win, DEFAULT_THEME_ID); + let enabledItems = card.options.visibleItems; + is(enabledItems.length, 1, "There is one enabled item"); + is(enabledItems[0].getAttribute("action"), "expand", "Expand is enabled"); + let moreOptionsButton = card.querySelector(".more-options-button"); + ok(!moreOptionsButton.hidden, "The more options button is visible"); + + let loaded = waitForViewLoad(win); + enabledItems[0].click(); + await loaded; + + card = getAddonCard(win, DEFAULT_THEME_ID); + let toggleDisabledButton = card.querySelector('[action="toggle-disabled"]'); + enabledItems = card.options.visibleItems; + is(enabledItems.length, 0, "There are no enabled items"); + moreOptionsButton = card.querySelector(".more-options-button"); + ok(moreOptionsButton.hidden, "The more options button is now hidden"); + ok(toggleDisabledButton.hidden, "The disable button is hidden"); + + // Switch themes, the menu should be hidden, but enable button should appear. + let darkTheme = await AddonManager.getAddonByID(DARK_THEME_ID); + let updated = BrowserTestUtils.waitForEvent(card, "update"); + await darkTheme.enable(); + await updated; + + ok(moreOptionsButton.hidden, "The more options button is still hidden"); + ok(!toggleDisabledButton.hidden, "The enable button is visible"); + + updated = BrowserTestUtils.waitForEvent(card, "update"); + await toggleDisabledButton.click(); + await updated; + + ok(moreOptionsButton.hidden, "The more options button is hidden"); + ok(toggleDisabledButton.hidden, "The disable button is hidden"); + + await closeView(win); +}); + +add_task(async function testGoBackButtonIsDisabledWhenHistoryIsEmpty() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { name: "Test Go Back Button" }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let viewID = `addons://detail/${encodeURIComponent(extension.id)}`; + + // When we have a fresh new tab, `about:addons` is opened in it. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, null); + // Simulate a click on "Manage extension" from a context menu. + let win = await BrowserOpenAddonsMgr(viewID); + await assertBackButtonIsDisabled(win); + + BrowserTestUtils.removeTab(tab); + await extension.unload(); +}); + +add_task(async function testGoBackButtonIsDisabledWhenHistoryIsEmptyInNewTab() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { name: "Test Go Back Button" }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let viewID = `addons://detail/${encodeURIComponent(extension.id)}`; + + // When we have a tab with a page loaded, `about:addons` will be opened in a + // new tab. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.org" + ); + let addonsTabLoaded = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:addons", + true + ); + // Simulate a click on "Manage extension" from a context menu. + let win = await BrowserOpenAddonsMgr(viewID); + let addonsTab = await addonsTabLoaded; + await assertBackButtonIsDisabled(win); + + BrowserTestUtils.removeTab(addonsTab); + BrowserTestUtils.removeTab(tab); + await extension.unload(); +}); + +add_task(async function testGoBackButtonIsDisabledAfterBrowserBackButton() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { name: "Test Go Back Button" }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let viewID = `addons://detail/${encodeURIComponent(extension.id)}`; + + // When we have a fresh new tab, `about:addons` is opened in it. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, null); + // Simulate a click on "Manage extension" from a context menu. + let win = await BrowserOpenAddonsMgr(viewID); + await assertBackButtonIsDisabled(win); + + // Navigate to the extensions list. + await new CategoryUtilities(win).openType("extension"); + + // Click on the browser back button. + gBrowser.goBack(); + await assertBackButtonIsDisabled(win); + + BrowserTestUtils.removeTab(tab); + await extension.unload(); +}); + +add_task(async function testQuarantinedDomainsUserAllowedUI() { + let regularExtId = "regular@mochi.test"; + let privilegedExtId = "privileged@mochi.test"; + let recommendedExtId = "recommended@mochi.test"; + let themeId = "theme@mochi.test"; + let provider = new MockProvider(); + provider.createAddons([ + { + id: privilegedExtId, + isPrivileged: true, + name: "A privileged extension", + type: "extension", + quarantineIgnoredByApp: true, + quarantineIgnoredByUser: false, + canChangeQuarantineIgnored: false, + }, + { + id: recommendedExtId, + isRecommended: true, + recommendationStates: ["recommended"], + name: "A Recommended extension", + type: "extension", + quarantineIgnoredByApp: true, + quarantineIgnoredByUser: false, + canChangeQuarantineIgnored: false, + }, + { + id: themeId, + name: "A fake regular theme", + type: "theme", + canChangeQuarantineIgnored: false, + }, + ]); + + let regularExtension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Some regular extension", + browser_specific_settings: { gecko: { id: regularExtId } }, + }, + useAddonManager: "permanent", + }); + + async function testQuarantinedUserAllowedUIRows(id, { expectVisible }) { + const perAddonPref = QuarantinedDomains.getUserAllowedAddonIdPrefName(id); + Services.prefs.clearUserPref(perAddonPref); + + let card = getAddonCard(win, id); + + const cardDetails = card.querySelector("addon-details"); + ok(cardDetails, "Card details found"); + const quarantinedUserAllowedControlsRow = cardDetails.querySelector( + ".addon-detail-row-quarantined-domains" + ); + + ok( + quarantinedUserAllowedControlsRow, + "Found quarantine domains controls row element" + ); + + is( + BrowserTestUtils.isVisible(quarantinedUserAllowedControlsRow), + expectVisible, + `Expect quarantineIgnoreByUser UI to ${ + expectVisible ? "be" : "NOT be" + } visible` + ); + const helpRow = quarantinedUserAllowedControlsRow.nextElementSibling; + is( + helpRow.classList.contains("addon-detail-help-row"), + true, + "Expect next sibling to be an addon-detail-help-row" + ); + is( + BrowserTestUtils.isVisible(helpRow), + expectVisible, + `Expect quarantineIgnoredByUser UI help to ${ + expectVisible ? "be" : "NOT be" + } visible` + ); + + if (!expectVisible) { + // The assertion that follows are going to be executed when the + // test helper function is called for an addon card detail view + // for which the quarantined domains rows are expected to be + // visible. + return; + } + + is( + doc.l10n.getAttributes(helpRow.firstElementChild).id, + "addon-detail-quarantined-domains-help", + "Expect addon-detail-help-row to be localized" + ); + const helpSupportLink = helpRow.querySelector("[is=moz-support-link]"); + ok(helpSupportLink, "Expect a moz-support-link"); + is( + helpSupportLink?.getAttribute("support-page"), + "quarantined-domains", + "Expect support link to point to SUMO quarantined-domains page" + ); + // Make sure none of the elements in the help row are missing + // the expected strings associated to the fluent ids being set + // (if any is missing, l10n.translateElements will reject and + // trigger an explicit test failure); + await doc.l10n.translateElements([helpRow]); + + const radioInputs = Array.from( + quarantinedUserAllowedControlsRow.querySelectorAll( + "input[name=quarantined-domains-user-allowed]" + ) + ); + + Assert.deepEqual( + radioInputs.map(el => el.value), + ["1", "0"], + "Got the expected radio inputs values" + ); + + Assert.deepEqual( + radioInputs.map(el => doc.l10n.getAttributes(el.nextElementSibling).id), + ["allow", "disallow"].map( + txt => `addon-detail-quarantined-domains-${txt}` + ), + "Got the expected fluent ids on the radio input text" + ); + + const checkRadioInputsState = ({ expectUserAllowed }) => { + is( + card.addon.quarantineIgnoredByUser, + expectUserAllowed, + `Expect the test extension to ${ + expectUserAllowed ? "be" : "NOT be" + } quarantineIgnoredByUser` + ); + is( + radioInputs[0].checked, + expectUserAllowed, + `Expect 'allow' radio button to ${ + expectUserAllowed ? "be" : "NOT be" + } checked` + ); + is( + radioInputs[1].checked, + !expectUserAllowed, + `Expect 'disallow' radio button ${ + expectUserAllowed ? "NOT be" : "be" + } checked` + ); + }; + + info("Verify initially NOT allowed to access quarantine domains"); + checkRadioInputsState({ expectUserAllowed: false }); + + info("Click 'allow' radio input"); + radioInputs[0].click(); + checkRadioInputsState({ expectUserAllowed: true }); + + info("Click 'disallow' radio input"); + radioInputs[1].click(); + checkRadioInputsState({ expectUserAllowed: false }); + + info("Verify quarantineIgnoredByUser changes reflected in about:addons UI"); + + info("Allow test extension on quarantined domains"); + let promisePropertyChanged = + AddonTestUtils.promiseAddonEvent("onPropertyChanged"); + card.addon.quarantineIgnoredByUser = true; + await promisePropertyChanged; + checkRadioInputsState({ expectUserAllowed: true }); + + info("Disallow test extension on quarantined domains"); + promisePropertyChanged = + AddonTestUtils.promiseAddonEvent("onPropertyChanged"); + card.addon.quarantineIgnoredByUser = false; + await promisePropertyChanged; + checkRadioInputsState({ expectUserAllowed: false }); + } + + await SpecialPowers.pushPrefEnv({ + set: [ + // Make sure the quarantined domains feature is initially enabled + // otherwise the "quarantineIgnoredByUser UI" rows are + // going to be hidden. + ["extensions.quarantinedDomains.enabled", true], + // Make sure this test is always running with the + // "per-addon quarantineIgnoredByUser UI" feature enabled. + ["extensions.quarantinedDomains.uiDisabled", false], + ], + }); + + // Clear any per-addon pref once this test file is exiting. + registerCleanupFunction(() => { + const prefBranch = Services.prefs.getBranch( + QuarantinedDomains.PREF_ADDONS_BRANCH_NAME + ); + for (const leafName of prefBranch.getChildList("")) { + const prefName = QuarantinedDomains.PREF_ADDONS_BRANCH_NAME + leafName; + info(`Clearing user pref ${prefName}`); + Services.prefs.clearUserPref(prefName); + } + }); + + await regularExtension.startup(); + + let win = await loadInitialView("extension"); + let doc = win.document; + + info("Test quarantineIgnoredByUser UI on a regular extension"); + let loaded = waitForViewLoad(win); + getAddonCard(win, regularExtId).querySelector('[action="expand"]').click(); + await loaded; + + await testQuarantinedUserAllowedUIRows(regularExtId, { expectVisible: true }); + + info("Go back to extensions list view"); + loaded = waitForViewLoad(win); + win.history.back(); + await loaded; + + info("Test quarantineIgnoredByUser UI on a privileged extension"); + loaded = waitForViewLoad(win); + getAddonCard(win, privilegedExtId).querySelector('[action="expand"]').click(); + await loaded; + + await testQuarantinedUserAllowedUIRows(privilegedExtId, { + expectVisible: false, + }); + + info("Go back to extensions list view"); + loaded = waitForViewLoad(win); + win.history.back(); + await loaded; + + info("Test quarantineIgnoredByUser UI on a recommended extension"); + loaded = waitForViewLoad(win); + getAddonCard(win, recommendedExtId) + .querySelector('[action="expand"]') + .click(); + await loaded; + + await testQuarantinedUserAllowedUIRows(recommendedExtId, { + expectVisible: false, + }); + + info("Switch to theme list view"); + loaded = waitForViewLoad(win); + doc.querySelector("#categories > [name=theme]").click(); + await loaded; + + info("Test quarantineIgnoredByUser UI on a non extension addon type (theme)"); + loaded = waitForViewLoad(win); + getAddonCard(win, themeId).querySelector('[action="expand"]').click(); + await loaded; + + await testQuarantinedUserAllowedUIRows(themeId, { expectVisible: false }); + + info("Verify regular extension card on quarantined domains feature disabled"); + await SpecialPowers.pushPrefEnv({ + set: [["extensions.quarantinedDomains.enabled", false]], + }); + + info("Switch to extension list view"); + loaded = waitForViewLoad(win); + doc.querySelector("#categories > [name=extension]").click(); + await loaded; + + loaded = waitForViewLoad(win); + getAddonCard(win, regularExtId).querySelector('[action="expand"]').click(); + await loaded; + + await testQuarantinedUserAllowedUIRows(regularExtId, { + expectVisible: false, + }); + + await SpecialPowers.popPrefEnv(); + + info("Verify regular extenson card uiDisabled pref set to true"); + await SpecialPowers.pushPrefEnv({ + set: [ + // Make sure the quarantineIgnoredByUser UI is also hidden + // when the quarantine domains feature is enabled but the + // "per-addon quarantineIgnoredByUser UI" feature is disabled. + ["extensions.quarantinedDomains.uiDisabled", true], + ], + }); + + info("Switch to extension list view"); + loaded = waitForViewLoad(win); + doc.querySelector("#categories > [name=extension]").click(); + await loaded; + + loaded = waitForViewLoad(win); + getAddonCard(win, regularExtId).querySelector('[action="expand"]').click(); + await loaded; + + await testQuarantinedUserAllowedUIRows(regularExtId, { + expectVisible: false, + }); + + await closeView(win); + await regularExtension.unload(); + await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function testRatingsElementVisibleIfReviewURLExists() { + let win = await loadInitialView("extension"); + let id = "addon4@mochi.test"; + let card = getAddonCard(win, id); + + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + card = getAddonCard(win, id); + + let rows = getDetailRows(card); + + let expectedRowCount = 5; + if (card.addon.canChangeQuarantineIgnored) { + expectedRowCount += 2; + } + is(rows.length, expectedRowCount, "Expected row count"); + + // Reviews. + // addon4@mochi.test is similar to addon1@mochi.test whose rows have already + // been checked in testFullDetails. Here we only check the last row + // which is unique to this test case due to the presence of "reviewURL". + let row = rows.pop(); + await checkLabel(row, "rating"); + let rating = row.lastElementChild; + ok(rating.classList.contains("addon-detail-rating"), "Found the rating el"); + ok(!row.hidden, "The rating row is shown"); + let mozFiveStar = rating.querySelector("moz-five-star"); + is(mozFiveStar.rating, 0, "0 rating when there are no reviews"); + let stars = Array.from(mozFiveStar.starEls); + let fullAttrs = stars.map(star => star.getAttribute("fill")).join(","); + is(fullAttrs, "empty,empty,empty,empty,empty", "All stars are empty"); + let link = rating.querySelector("a"); + let reviewsLink = formatUrl( + "addons-manager-reviews-link", + "http://addons.mozilla.org/reviews" + ); + checkLink(link, reviewsLink, { + id: "addon-detail-reviews-link", + args: { numberOfReviews: 0 }, + }); + + await closeView(win); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js new file mode 100644 index 0000000000..bc84ffaf89 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js @@ -0,0 +1,668 @@ +/* eslint max-len: ["error", 80] */ +"use strict"; + +loadTestSubscript("head_disco.js"); + +// The response to the discovery API, as documented at: +// https://addons-server.readthedocs.io/en/latest/topics/api/discovery.html +// +// The test is designed to easily verify whether the discopane works with the +// latest AMO API, by replacing API_RESPONSE_FILE's content with latest AMO API +// response, e.g. from https://addons.allizom.org/api/v4/discovery/?lang=en-US +// The response must contain at least one theme, and one extension. + +const API_RESPONSE_FILE = PathUtils.join( + Services.dirsvc.get("CurWorkD", Ci.nsIFile).path, + // Trim empty component from splitting with trailing slash. + ...RELATIVE_DIR.split("/").filter(c => c.length), + "discovery", + "api_response.json" +); + +const AMO_TEST_HOST = "rewritten-for-testing.addons.allizom.org"; + +const ArrayBufferInputStream = Components.Constructor( + "@mozilla.org/io/arraybuffer-input-stream;1", + "nsIArrayBufferInputStream", + "setData" +); + +const amoServer = AddonTestUtils.createHttpServer({ hosts: [AMO_TEST_HOST] }); + +amoServer.registerFile( + "/png", + new FileUtils.File( + PathUtils.join( + Services.dirsvc.get("CurWorkD", Ci.nsIFile).path, + ...`${RELATIVE_DIR}discovery/small-1x1.png`.split("/") + ) + ) +); +amoServer.registerPathHandler("/dummy", (request, response) => { + response.write("Dummy"); +}); + +// `result` is an element in the `results` array from AMO's discovery API, +// stored in API_RESPONSE_FILE. +function getTestExpectationFromApiResult(result) { + return { + typeIsTheme: result.addon.type === "statictheme", + addonName: result.addon.name, + authorName: result.addon.authors[0].name, + editorialBody: result.description_text, + dailyUsers: result.addon.average_daily_users, + rating: result.addon.ratings.average, + }; +} + +// A helper to declare a response to discovery API requests. +class DiscoveryAPIHandler { + constructor(responseText) { + this.setResponseText(responseText); + this.requestCount = 0; + + // Overwrite the previous discovery response handler. + amoServer.registerPathHandler("/discoapi", this); + } + + setResponseText(responseText) { + this.responseBody = new TextEncoder().encode(responseText).buffer; + } + + // Suspend discovery API requests until unblockResponses is called. + blockNextResponses() { + this._unblockPromise = new Promise(resolve => { + this.unblockResponses = resolve; + }); + } + + unblockResponses(responseText) { + throw new Error("You need to call blockNextResponses first!"); + } + + // nsIHttpRequestHandler::handle + async handle(request, response) { + ++this.requestCount; + + response.setHeader("Cache-Control", "no-cache", false); + response.processAsync(); + await this._unblockPromise; + + let body = this.responseBody; + let binStream = new ArrayBufferInputStream(body, 0, body.byteLength); + response.bodyOutputStream.writeFrom(binStream, body.byteLength); + response.finish(); + } +} + +// Retrieve the list of visible action elements inside a document or container. +function getVisibleActions(documentOrElement) { + return Array.from(documentOrElement.querySelectorAll("[action]")).filter( + elem => + elem.getAttribute("action") !== "page-options" && + elem.offsetWidth && + elem.offsetHeight + ); +} + +function getActionName(actionElement) { + return actionElement.getAttribute("action"); +} + +function getCardByAddonId(win, addonId) { + for (let card of win.document.querySelectorAll("recommended-addon-card")) { + if (card.addonId === addonId) { + return card; + } + } + return null; +} + +// Switch to a different view so we can switch back to the discopane later. +async function switchToNonDiscoView(win) { + // Listeners registered while the discopane was the active view continue to be + // active when the view switches to the extensions list, because both views + // share the same document. + win.gViewController.loadView("addons://list/extension"); + await wait_for_view_load(win); + ok( + win.document.querySelector("addon-list"), + "Should be at the extension list view" + ); +} + +// Switch to the discopane and wait until it has fully rendered, including any +// cards from the discovery API. +async function switchToDiscoView(win) { + is( + getDiscoveryElement(win), + null, + "Cannot switch to discopane when the discopane is already shown" + ); + win.gViewController.loadView("addons://discover/"); + await wait_for_view_load(win); + await promiseDiscopaneUpdate(win); +} + +// Wait until all images in the DOM have successfully loaded. +// There must be at least one `<img>` in the document. +// Returns the number of loaded images. +async function waitForAllImagesLoaded(win) { + let imgs = Array.from( + win.document.querySelectorAll("discovery-pane img[src]") + ); + function areAllImagesLoaded() { + let loadCount = imgs.filter(img => img.naturalWidth).length; + info(`Loaded ${loadCount} out of ${imgs.length} images`); + return loadCount === imgs.length; + } + if (!areAllImagesLoaded()) { + await promiseEvent(win.document, "load", true, areAllImagesLoaded); + } + return imgs.length; +} + +// Install an add-on by clicking on the card. +// The promise resolves once the card has been updated. +async function testCardInstall(card) { + Assert.deepEqual( + getVisibleActions(card).map(getActionName), + ["install-addon"], + "Should have an Install button before install" + ); + + let installButton = + card.querySelector("[data-l10n-id='install-extension-button']") || + card.querySelector("[data-l10n-id='install-theme-button']"); + + let updatePromise = promiseEvent(card, "disco-card-updated"); + installButton.click(); + await updatePromise; + + Assert.deepEqual( + getVisibleActions(card).map(getActionName), + ["manage-addon"], + "Should have a Manage button after install" + ); +} + +// Uninstall the add-on (not via the card, since it has no uninstall button). +// The promise resolves once the card has been updated. +async function testAddonUninstall(card) { + Assert.deepEqual( + getVisibleActions(card).map(getActionName), + ["manage-addon"], + "Should have a Manage button before uninstall" + ); + + let addon = await AddonManager.getAddonByID(card.addonId); + + let updatePromise = promiseEvent(card, "disco-card-updated"); + await addon.uninstall(); + await updatePromise; + + Assert.deepEqual( + getVisibleActions(card).map(getActionName), + ["install-addon"], + "Should have an Install button after uninstall" + ); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "extensions.getAddons.discovery.api_url", + `http://${AMO_TEST_HOST}/discoapi`, + ], + // Disable non-discopane recommendations to avoid unexpected discovery + // API requests. + ["extensions.htmlaboutaddons.recommendations.enabled", false], + // Disable the telemetry client ID (and its associated UI warning). + // browser_html_discover_view_clientid.js covers this functionality. + ["browser.discovery.enabled", false], + // Disable mixed-content upgrading as this test is expecting an HTTP load + ["security.mixed_content.upgrade_display_content", false], + ], + }); +}); + +// Test that the discopane can be loaded and that meaningful results are shown. +// This relies on response data from the AMO API, stored in API_RESPONSE_FILE. +add_task(async function discopane_with_real_api_data() { + const apiText = await readAPIResponseFixture( + AMO_TEST_HOST, + API_RESPONSE_FILE + ); + let apiHandler = new DiscoveryAPIHandler(apiText); + + const apiResultArray = JSON.parse(apiText).results; + ok(apiResultArray.length, `Mock has ${apiResultArray.length} results`); + + apiHandler.blockNextResponses(); + let win = await loadInitialView("discover"); + + Assert.deepEqual( + getVisibleActions(win.document).map(getActionName), + [], + "The AMO button should be invisible when the AMO API hasn't responded" + ); + + apiHandler.unblockResponses(); + await promiseDiscopaneUpdate(win); + + let actionElements = getVisibleActions(win.document); + Assert.deepEqual( + actionElements.map(getActionName), + [ + // Expecting an install button for every result. + ...new Array(apiResultArray.length).fill("install-addon"), + "open-amo", + ], + "All add-on cards should be rendered, with AMO button at the end." + ); + + let imgCount = await waitForAllImagesLoaded(win); + is(imgCount, apiResultArray.length, "Expected an image for every result"); + + // Check that the cards have the expected content. + let cards = Array.from( + win.document.querySelectorAll("recommended-addon-card") + ); + is(cards.length, apiResultArray.length, "Every API result has a card"); + for (let [i, card] of cards.entries()) { + let expectations = getTestExpectationFromApiResult(apiResultArray[i]); + info(`Expectations for card ${i}: ${JSON.stringify(expectations)}`); + + let checkContent = (selector, expectation) => { + let text = card.querySelector(selector).textContent; + is(text, expectation, `Content of selector "${selector}"`); + }; + checkContent(".disco-addon-name", expectations.addonName); + await win.document.l10n.translateFragment(card); + checkContent( + ".disco-addon-author [data-l10n-name='author']", + expectations.authorName + ); + + let amoListingLink = card.querySelector(".disco-addon-author a"); + ok( + amoListingLink.search.includes("utm_source=firefox-browser"), + `Listing link should have attribution parameter, url=${amoListingLink}` + ); + + let actions = getVisibleActions(card); + is(actions.length, 1, "Card should only have one install button"); + let installButton = actions[0]; + if (expectations.typeIsTheme) { + // Theme button + screenshot + ok( + installButton.matches("[data-l10n-id='install-theme-button'"), + "Has theme install button" + ); + ok( + card.querySelector(".card-heading-image").offsetWidth, + "Preview image must be visible" + ); + } else { + // Extension button + extended description. + ok( + installButton.matches("[data-l10n-id='install-extension-button'"), + "Has extension install button" + ); + checkContent(".disco-description-main", expectations.editorialBody); + + let mozFiveStar = card.querySelector("moz-five-star"); + if (expectations.rating) { + is(mozFiveStar.rating, expectations.rating, "Expected rating value"); + ok(mozFiveStar.offsetWidth, "Rating element is visible"); + } else { + is(mozFiveStar.offsetWidth, 0, "Rating element is not visible"); + } + + let userCountElem = card.querySelector(".disco-user-count"); + if (expectations.dailyUsers) { + Assert.deepEqual( + win.document.l10n.getAttributes(userCountElem), + { id: "user-count", args: { dailyUsers: expectations.dailyUsers } }, + "Card count should be rendered" + ); + } else { + is(userCountElem.offsetWidth, 0, "User count element is not visible"); + } + } + } + + is(apiHandler.requestCount, 1, "Discovery API should be fetched once"); + + await closeView(win); +}); + +// Test whether extensions and themes can be installed from the discopane. +// Also checks that items in the list do not change position after installation, +// and that they are shown at the bottom of the list when the discopane is +// reopened. +add_task(async function install_from_discopane() { + const apiText = await readAPIResponseFixture( + AMO_TEST_HOST, + API_RESPONSE_FILE + ); + const apiResultArray = JSON.parse(apiText).results; + let getAddonIdByAMOAddonType = type => + apiResultArray.find(r => r.addon.type === type).addon.guid; + const FIRST_EXTENSION_ID = getAddonIdByAMOAddonType("extension"); + const FIRST_THEME_ID = getAddonIdByAMOAddonType("statictheme"); + + let apiHandler = new DiscoveryAPIHandler(apiText); + + let win = await loadInitialView("discover"); + await promiseDiscopaneUpdate(win); + await waitForAllImagesLoaded(win); + + // Test extension install. + let installExtensionPromise = promiseAddonInstall(amoServer, { + manifest: { + name: "My Awesome Add-on", + description: "Test extension install button", + browser_specific_settings: { gecko: { id: FIRST_EXTENSION_ID } }, + permissions: ["<all_urls>"], + }, + }); + await testCardInstall(getCardByAddonId(win, FIRST_EXTENSION_ID)); + await installExtensionPromise; + + // Test theme install. + let installThemePromise = promiseAddonInstall(amoServer, { + manifest: { + name: "My Fancy Theme", + description: "Test theme install button", + browser_specific_settings: { gecko: { id: FIRST_THEME_ID } }, + theme: { + colors: { + tab_selected: "red", + }, + }, + }, + }); + let promiseThemeChange = promiseObserved("lightweight-theme-styling-update"); + await testCardInstall(getCardByAddonId(win, FIRST_THEME_ID)); + await installThemePromise; + await promiseThemeChange; + + // After installing, the cards should have manage buttons instead of install + // buttons. The cards should still be at the top of the pane (and not be + // moved to the bottom). + Assert.deepEqual( + getVisibleActions(win.document).map(getActionName), + [ + "manage-addon", + "manage-addon", + ...new Array(apiResultArray.length - 2).fill("install-addon"), + "open-amo", + ], + "The Install buttons should be replaced with Manage buttons" + ); + + // End of the testing installation from a card. + + // Click on the Manage button to verify that it does something useful, + // and in order to be able to force the discovery pane to be rendered again. + let loaded = waitForViewLoad(win); + getCardByAddonId(win, FIRST_EXTENSION_ID) + .querySelector("[action='manage-addon']") + .click(); + await loaded; + { + let addonCard = win.document.querySelector( + `addon-card[addon-id="${FIRST_EXTENSION_ID}"]` + ); + ok(addonCard, "Add-on details should be shown"); + ok(addonCard.expanded, "The card should have been expanded"); + // TODO bug 1540253: Check that the "recommended" badge is visible. + } + + // Now we are going to force an updated rendering and check that the cards are + // in the expected order, and then test uninstallation of the above add-ons. + await switchToDiscoView(win); + await waitForAllImagesLoaded(win); + + Assert.deepEqual( + getVisibleActions(win.document).map(getActionName), + [ + ...new Array(apiResultArray.length - 2).fill("install-addon"), + "manage-addon", + "manage-addon", + "open-amo", + ], + "Already-installed add-ons should be rendered at the end of the list" + ); + + promiseThemeChange = promiseObserved("lightweight-theme-styling-update"); + await testAddonUninstall(getCardByAddonId(win, FIRST_THEME_ID)); + await promiseThemeChange; + await testAddonUninstall(getCardByAddonId(win, FIRST_EXTENSION_ID)); + + is(apiHandler.requestCount, 1, "Discovery API should be fetched once"); + + await closeView(win); +}); + +// Tests that the page is able to switch views while the discopane is loading, +// without inadvertently replacing the page when the request finishes. +add_task(async function discopane_navigate_while_loading() { + let apiHandler = new DiscoveryAPIHandler(`{"results": []}`); + + apiHandler.blockNextResponses(); + let win = await loadInitialView("discover"); + + let updatePromise = promiseDiscopaneUpdate(win); + let didUpdateDiscopane = false; + updatePromise.then(() => { + didUpdateDiscopane = true; + }); + + // Switch views while the request is pending. + await switchToNonDiscoView(win); + + is( + didUpdateDiscopane, + false, + "discopane should still not be updated because the request is blocked" + ); + is( + getDiscoveryElement(win), + null, + "Discopane should be removed after switching to the extension list" + ); + + // Release pending requests, to verify that completing the request will not + // cause changes to the visible view. The updatePromise will still resolve + // though, because the event is dispatched to the removed `<discovery-pane>`. + apiHandler.unblockResponses(); + + await updatePromise; + ok( + win.document.querySelector("addon-list"), + "Should still be at the extension list view" + ); + is( + getDiscoveryElement(win), + null, + "Discopane should not be in the document when it is not the active view" + ); + + is(apiHandler.requestCount, 1, "Discovery API should be fetched once"); + + await closeView(win); +}); + +// Tests that invalid responses are handled correctly and not cached. +// Also verifies that the response is cached as long as the page is active, +// but not when the page is fully reloaded. +add_task(async function discopane_cache_api_responses() { + const INVALID_RESPONSE_BODY = `{"This is some": invalid} JSON`; + let apiHandler = new DiscoveryAPIHandler(INVALID_RESPONSE_BODY); + + let expectedErrMsg; + try { + JSON.parse(INVALID_RESPONSE_BODY); + ok(false, "JSON.parse should have thrown"); + } catch (e) { + expectedErrMsg = e.message; + } + + let invalidResponseHandledPromise = new Promise(resolve => { + Services.console.registerListener(function listener(msg) { + if (msg.message.includes(expectedErrMsg)) { + resolve(); + Services.console.unregisterListener(listener); + } + }); + }); + + let win = await loadInitialView("discover"); // Request #1 + await promiseDiscopaneUpdate(win); + + info("Waiting for expected error"); + await invalidResponseHandledPromise; + is(apiHandler.requestCount, 1, "Discovery API should be fetched once"); + + Assert.deepEqual( + getVisibleActions(win.document).map(getActionName), + ["open-amo"], + "The AMO button should be visible even when the response was invalid" + ); + + // Change to a valid response, so that the next response will be cached. + apiHandler.setResponseText(`{"results": []}`); + + await switchToNonDiscoView(win); + await switchToDiscoView(win); // Request #2 + + is( + apiHandler.requestCount, + 2, + "Should fetch new data because an invalid response should not be cached" + ); + + await switchToNonDiscoView(win); + await switchToDiscoView(win); + await closeView(win); + + is( + apiHandler.requestCount, + 2, + "The previous response was valid and should have been reused" + ); + + // Now open a new about:addons page and verify that a new API request is sent. + let anotherWin = await loadInitialView("discover"); + await promiseDiscopaneUpdate(anotherWin); + await closeView(anotherWin); + + is(apiHandler.requestCount, 3, "discovery API should be requested again"); +}); + +add_task(async function discopane_no_cookies() { + let requestPromise = new Promise(resolve => { + amoServer.registerPathHandler("/discoapi", resolve); + }); + Services.cookies.add( + AMO_TEST_HOST, + "/", + "name", + "value", + false, + false, + false, + Date.now() / 1000 + 600, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTP + ); + let win = await loadInitialView("discover"); + let request = await requestPromise; + ok(!request.hasHeader("Cookie"), "discovery API should not receive cookies"); + await closeView(win); +}); + +// The CSP of about:addons whitelists http:, but not data:, hence we are +// loading a little red data: image which gets blocked by the CSP. +add_task(async function csp_img_src() { + const RED_DATA_IMAGE = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAA" + + "AHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; + + // Minimal API response to get the image in recommended-addon-card to render. + const DUMMY_EXTENSION_ID = "dummy-csp@extensionid"; + const apiResponse = { + results: [ + { + addon: { + guid: DUMMY_EXTENSION_ID, + type: "extension", + authors: [ + { + name: "Some CSP author", + }, + ], + url: `http://${AMO_TEST_HOST}/dummy`, + icon_url: RED_DATA_IMAGE, + }, + }, + ], + }; + + let apiHandler = new DiscoveryAPIHandler(JSON.stringify(apiResponse)); + apiHandler.blockNextResponses(); + let win = await loadInitialView("discover"); + + let cspPromise = new Promise(resolve => { + win.addEventListener("securitypolicyviolation", e => { + // non http(s) loads only report the scheme + is(e.blockedURI, "data", "CSP: blocked URI"); + is(e.violatedDirective, "img-src", "CSP: violated directive"); + resolve(); + }); + }); + + apiHandler.unblockResponses(); + await cspPromise; + + await closeView(win); +}); + +add_task(async function checkDiscopaneNotice() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.discovery.enabled", true], + // Enabling the Data Upload pref may upload data. + // Point data reporting services to localhost so the data doesn't escape. + ["toolkit.telemetry.server", "https://localhost:1337"], + ["telemetry.fog.test.localhost_port", -1], + ["datareporting.healthreport.uploadEnabled", true], + ["extensions.htmlaboutaddons.recommendations.enabled", true], + ["extensions.recommendations.hideNotice", false], + // Disable mixed-content upgrading as this test is expecting an HTTP load + ["security.mixed_content.upgrade_display_content", false], + ], + }); + + let win = await loadInitialView("extension"); + let messageBar = win.document.querySelector( + "moz-message-bar.discopane-notice" + ); + ok(messageBar, "Recommended notice should exist in extensions view"); + await switchToDiscoView(win); + messageBar = win.document.querySelector("moz-message-bar.discopane-notice"); + ok(messageBar, "Recommended notice should exist in disco view"); + + messageBar.closeButtonEl.click(); + messageBar = win.document.querySelector("moz-message-bar.discopane-notice"); + ok(!messageBar, "Recommended notice should not exist in disco view"); + await switchToNonDiscoView(win); + messageBar = win.document.querySelector("moz-message-bar.discopane-notice"); + ok(!messageBar, "Recommended notice should not exist in extensions view"); + + await closeView(win); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_discover_view_clientid.js b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view_clientid.js new file mode 100644 index 0000000000..ff95c88fe1 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view_clientid.js @@ -0,0 +1,219 @@ +/* eslint max-len: ["error", 80] */ +"use strict"; + +const { ClientID } = ChromeUtils.importESModule( + "resource://gre/modules/ClientID.sys.mjs" +); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); +const server = AddonTestUtils.createHttpServer(); +const serverBaseUrl = `http://localhost:${server.identity.primaryPort}/`; +server.registerPathHandler("/sumo/personalized-addons", (request, response) => { + response.write("This is a SUMO page that explains personalized add-ons."); +}); + +// Before a discovery API request is triggered, this method should be called. +// Resolves with the value of the "telemetry-client-id" query parameter. +async function promiseOneDiscoveryApiRequest() { + return new Promise(resolve => { + let requestCount = 0; + // Overwrite previous request handler, if any. + server.registerPathHandler("/discoapi", (request, response) => { + is(++requestCount, 1, "Expecting one discovery API request"); + response.write(`{"results": []}`); + let searchParams = new URLSearchParams(request.queryString); + let clientId = searchParams.get("telemetry-client-id"); + resolve(clientId); + }); + }); +} + +function getNoticeButton(win) { + return win.document.querySelector("[action='notice-learn-more']"); +} + +function isNoticeVisible(win) { + let message = win.document.querySelector("taar-notice"); + return message && message.offsetHeight > 0; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Enable clientid - see Discovery.sys.mjs for the first two prefs. + ["browser.discovery.enabled", true], + // Enabling the Data Upload pref may upload data. + // Point data reporting services to localhost so the data doesn't escape. + ["toolkit.telemetry.server", "https://localhost:1337"], + ["telemetry.fog.test.localhost_port", -1], + ["datareporting.healthreport.uploadEnabled", true], + ["extensions.getAddons.discovery.api_url", `${serverBaseUrl}discoapi`], + ["app.support.baseURL", `${serverBaseUrl}sumo/`], + // Discovery API requests can be triggered by the discopane and the + // recommendations in the list view. To make sure that the every test + // checks the behavior of the view they're testing, ensure that only one + // of the two views is enabled at a time. + ["extensions.htmlaboutaddons.recommendations.enabled", false], + ], + }); +}); + +// Test that the clientid is passed to the API when enabled via prefs. +add_task(async function clientid_enabled() { + let EXPECTED_CLIENT_ID = await ClientID.getClientIdHash(); + ok(EXPECTED_CLIENT_ID, "ClientID should be available"); + + let requestPromise = promiseOneDiscoveryApiRequest(); + let win = await loadInitialView("discover"); + + ok(isNoticeVisible(win), "Notice about personalization should be visible"); + + // TODO: This should ideally check whether the result is the expected ID. + // But run with --verify, the test may fail with EXPECTED_CLIENT_ID being + // "baae8d197cf6b0865d7ba7ddf83829cd2d9844374d7271a5c704199d91059316", + // which is sha256(TelemetryUtils.knownClientId). + // This happens because at the end of the test, the pushPrefEnv from setup is + // reverted, which resets datareporting.healthreport.uploadEnabled to false. + // When TelemetryController.sys.mjs detects this, it asynchronously resets the + // ClientID to knownClientId - which may happen at the next run of the test. + // TODO: Fix this together with bug 1537933 + // + // is(await requestPromise, EXPECTED_CLIENT_ID, + ok( + await requestPromise, + "Moz-Client-Id should be set when telemetry & discovery are enabled" + ); + + let tabbrowser = win.windowRoot.ownerGlobal.gBrowser; + let expectedUrl = `${serverBaseUrl}sumo/personalized-addons`; + let tabPromise = BrowserTestUtils.waitForNewTab(tabbrowser, expectedUrl); + + getNoticeButton(win).click(); + + info(`Waiting for new tab with URL: ${expectedUrl}`); + let tab = await tabPromise; + BrowserTestUtils.removeTab(tab); + + await closeView(win); +}); + +// Test that the clientid is not sent when disabled via prefs. +add_task(async function clientid_disabled() { + // Temporarily override the prefs that we had set in setup. + await SpecialPowers.pushPrefEnv({ + set: [["browser.discovery.enabled", false]], + }); + let requestPromise = promiseOneDiscoveryApiRequest(); + let win = await loadInitialView("discover"); + ok(!isNoticeVisible(win), "Notice about personalization should be hidden"); + is( + await requestPromise, + null, + "Moz-Client-Id should not be sent when discovery is disabled" + ); + await closeView(win); + await SpecialPowers.popPrefEnv(); +}); + +// Test that the clientid is not sent from private windows. +add_task(async function clientid_from_private_window() { + let privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + let requestPromise = promiseOneDiscoveryApiRequest(); + let managerWindow = await open_manager( + "addons://discover/", + null, + null, + null, + privateWindow + ); + ok( + PrivateBrowsingUtils.isContentWindowPrivate(managerWindow), + "Addon-manager is in a private window" + ); + + is( + await requestPromise, + null, + "Moz-Client-Id should not be sent in private windows" + ); + + await close_manager(managerWindow); + await BrowserTestUtils.closeWindow(privateWindow); +}); + +add_task(async function clientid_enabled_from_extension_list() { + await SpecialPowers.pushPrefEnv({ + // Override prefs from setup to enable recommendations. + set: [ + ["extensions.htmlaboutaddons.recommendations.enabled", true], + ["extensions.getAddons.showPane", false], + ], + }); + + // Force the extension list to be the first load. This pref will be + // overwritten once the view loads. + Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, "addons://list/extension"); + + let requestPromise = promiseOneDiscoveryApiRequest(); + let win = await loadInitialView("extension"); + + ok(isNoticeVisible(win), "Notice about personalization should be visible"); + + ok( + await requestPromise, + "Moz-Client-Id should be set when telemetry & discovery are enabled" + ); + + // Make sure switching to the theme view doesn't trigger another request. + await switchView(win, "theme"); + + // Wait until the request would have happened so promiseOneDiscoveryApiRequest + // can fail if it does. + let recommendations = win.document.querySelector("recommended-addon-list"); + await recommendations.loadCardsIfNeeded(); + + await closeView(win); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function clientid_enabled_from_theme_list() { + await SpecialPowers.pushPrefEnv({ + // Override prefs from setup to enable recommendations. + set: [ + ["extensions.htmlaboutaddons.recommendations.enabled", true], + ["extensions.getAddons.showPane", false], + ], + }); + + // Force the theme list to be the first load. This pref will be overwritten + // once the view loads. + Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, "addons://list/theme"); + + let requestPromise = promiseOneDiscoveryApiRequest(); + let win = await loadInitialView("theme"); + + ok(!isNoticeVisible(win), "Notice about personalization should be hidden"); + + is( + await requestPromise, + null, + "Moz-Client-Id should not be sent when loading themes initially" + ); + + info("Load the extension list and verify the client ID is now sent"); + + requestPromise = promiseOneDiscoveryApiRequest(); + await switchView(win, "extension"); + + ok(await requestPromise, "Moz-Client-Id is now sent for extensions"); + + await closeView(win); + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_discover_view_prefs.js b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view_prefs.js new file mode 100644 index 0000000000..474cd424b9 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view_prefs.js @@ -0,0 +1,83 @@ +/* eslint max-len: ["error", 80] */ +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); +const server = AddonTestUtils.createHttpServer(); +const TEST_API_URL = `http://localhost:${server.identity.primaryPort}/discoapi`; + +async function checkIfDiscoverVisible(expectVisible) { + let requestCount = 0; + let requestPromise = new Promise(resolve => { + // Overwrites previous request handler, if any. + server.registerPathHandler("/discoapi", (request, response) => { + ++requestCount; + response.write(`{"results": []}`); + resolve(); + }); + }); + + // Open about:addons with default view. + let managerWindow = await open_manager(null); + let categoryUtilities = new CategoryUtilities(managerWindow); + + is( + categoryUtilities.isTypeVisible("discover"), + expectVisible, + "Visibility of discopane" + ); + + await wait_for_view_load(managerWindow); + if (expectVisible) { + is( + categoryUtilities.selectedCategory, + "discover", + "Expected discopane as the default view" + ); + await requestPromise; + is(requestCount, 1, "Expected discovery API request"); + } else { + // The next view (after discopane) is the extension list. + is( + categoryUtilities.selectedCategory, + "extension", + "Should fall back to another view when the discopane is disabled" + ); + is(requestCount, 0, "Discovery API should not be requested"); + } + + await close_manager(managerWindow); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.getAddons.discovery.api_url", TEST_API_URL], + // Disable recommendations at the HTML about:addons view to avoid sending + // a discovery API request from the fallback view (extension list) in the + // showPane_false test. + ["extensions.htmlaboutaddons.recommendations.enabled", false], + ], + }); +}); + +add_task(async function showPane_true() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_DISCOVER_ENABLED, true]], + clear: [[PREF_UI_LASTCATEGORY]], + }); + await checkIfDiscoverVisible(true); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function showPane_false() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_DISCOVER_ENABLED, false]], + clear: [[PREF_UI_LASTCATEGORY]], + }); + await checkIfDiscoverVisible(false); + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_list_view.js b/toolkit/mozapps/extensions/test/browser/browser_html_list_view.js new file mode 100644 index 0000000000..2631a164df --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_list_view.js @@ -0,0 +1,1063 @@ +/* eslint max-len: ["error", 80] */ + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +let promptService; + +const SUPPORT_URL = Services.urlFormatter.formatURL( + Services.prefs.getStringPref("app.support.baseURL") +); +const REMOVE_SUMO_URL = SUPPORT_URL + "cant-remove-addon"; + +function getTestCards(root) { + return root.querySelectorAll('addon-card[addon-id$="@mochi.test"]'); +} + +function getCardByAddonId(root, id) { + return root.querySelector(`addon-card[addon-id="${id}"]`); +} + +function isEmpty(el) { + return !el.children.length; +} + +function waitForThemeChange(list) { + // Wait for two move events. One theme will be enabled and another disabled. + let moveCount = 0; + return BrowserTestUtils.waitForEvent(list, "move", () => ++moveCount == 2); +} + +let mockProvider; + +add_setup(async function () { + mockProvider = new MockProvider(["extension", "sitepermission"]); + promptService = mockPromptService(); +}); + +let extensionsCreated = 0; + +function createExtensions(manifestExtras) { + return manifestExtras.map(extra => + ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test extension", + browser_specific_settings: { + gecko: { id: `test-${extensionsCreated++}@mochi.test` }, + }, + icons: { + 32: "test-icon.png", + }, + ...extra, + }, + useAddonManager: "temporary", + }) + ); +} + +add_task(async function testExtensionList() { + let id = "test@mochi.test"; + let headingId = "test_mochi_test-heading"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test extension", + browser_specific_settings: { gecko: { id } }, + icons: { + 32: "test-icon.png", + }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let addon = await AddonManager.getAddonByID(id); + ok(addon, "The add-on can be found"); + + let win = await loadInitialView("extension"); + let doc = win.document; + + // Find the addon-list to listen for events. + let list = doc.querySelector("addon-list"); + + // There shouldn't be any disabled extensions. + let disabledSection = getSection(doc, "extension-disabled-section"); + ok(isEmpty(disabledSection), "The disabled section is empty"); + + // The loaded extension should be in the enabled list. + let enabledSection = getSection(doc, "extension-enabled-section"); + ok( + enabledSection && !isEmpty(enabledSection), + "The enabled section isn't empty" + ); + let card = getCardByAddonId(enabledSection, id); + ok(card, "The card is in the enabled section"); + + // Check the properties of the card. + is(card.addonNameEl.textContent, "Test extension", "The name is set"); + is( + card.querySelector("h3").id, + headingId, + "The add-on name has the correct id" + ); + is( + card.querySelector(".card").getAttribute("aria-labelledby"), + headingId, + "The card is labelled by the heading" + ); + let icon = card.querySelector(".addon-icon"); + ok(icon.src.endsWith("/test-icon.png"), "The icon is set"); + + // Disable the extension. + let disableToggle = card.querySelector('[action="toggle-disabled"]'); + ok(disableToggle.pressed, "The disable toggle is pressed"); + is( + doc.l10n.getAttributes(disableToggle).id, + "extension-enable-addon-button-label", + "The toggle has the enable label" + ); + ok(disableToggle.getAttribute("aria-label"), "There's an aria-label"); + ok(!disableToggle.hidden, "The toggle is visible"); + + let disabled = BrowserTestUtils.waitForEvent(list, "move"); + disableToggle.click(); + await disabled; + is( + card.parentNode, + disabledSection, + "The card is now in the disabled section" + ); + + // The disable button is now enabled. + ok(!disableToggle.pressed, "The disable toggle is not pressed"); + is( + doc.l10n.getAttributes(disableToggle).id, + "extension-enable-addon-button-label", + "The button has the same enable label" + ); + ok(disableToggle.getAttribute("aria-label"), "There's an aria-label"); + + // Remove the add-on. + let removeButton = card.querySelector('[action="remove"]'); + is( + doc.l10n.getAttributes(removeButton).id, + "remove-addon-button", + "The button has the remove label" + ); + // There is a support link when the add-on isn't removeable, verify we don't + // always include one. + ok(!removeButton.querySelector("a"), "There isn't a link in the item"); + + // Remove but cancel. + let cancelled = BrowserTestUtils.waitForEvent(card, "remove-cancelled"); + removeButton.click(); + await cancelled; + + let removed = BrowserTestUtils.waitForEvent(list, "remove"); + // Tell the mock prompt service that the prompt was accepted. + promptService._response = 0; + removeButton.click(); + await removed; + + addon = await AddonManager.getAddonByID(id); + ok( + addon && !!(addon.pendingOperations & AddonManager.PENDING_UNINSTALL), + "The addon is pending uninstall" + ); + + // Ensure that a pending uninstall bar has been created for the + // pending uninstall extension, and pressing the undo button will + // refresh the list and render a card to the re-enabled extension. + assertHasPendingUninstalls(list, 1); + assertHasPendingUninstallAddon(list, addon); + + // Add a second pending uninstall extension. + info("Install a second test extension and wait for addon card rendered"); + let added = BrowserTestUtils.waitForEvent(list, "add"); + const extension2 = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test extension 2", + browser_specific_settings: { gecko: { id: "test-2@mochi.test" } }, + icons: { + 32: "test-icon.png", + }, + }, + useAddonManager: "temporary", + }); + await extension2.startup(); + + await added; + ok( + getCardByAddonId(list, extension2.id), + "Got a card added for the second extension" + ); + + info("Uninstall the second test extension and wait for addon card removed"); + removed = BrowserTestUtils.waitForEvent(list, "remove"); + const addon2 = await AddonManager.getAddonByID(extension2.id); + addon2.uninstall(true); + await removed; + + ok( + !getCardByAddonId(list, extension2.id), + "Addon card for the second extension removed" + ); + + assertHasPendingUninstalls(list, 2); + assertHasPendingUninstallAddon(list, addon2); + + // Addon2 was enabled before entering the pending uninstall state, + // wait for its startup after pressing undo. + let addon2Started = AddonTestUtils.promiseWebExtensionStartup(addon2.id); + await testUndoPendingUninstall(list, addon); + await testUndoPendingUninstall(list, addon2); + info("Wait for the second pending uninstal add-ons startup"); + await addon2Started; + + ok( + getCardByAddonId(disabledSection, addon.id), + "The card for the first extension is in the disabled section" + ); + ok( + getCardByAddonId(enabledSection, addon2.id), + "The card for the second extension is in the enabled section" + ); + + await extension2.unload(); + await extension.unload(); + + // Install a theme and verify that it is not listed in the pending + // uninstall message bars while the list extensions view is loaded. + const themeXpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + name: "My theme", + browser_specific_settings: { gecko: { id: "theme@mochi.test" } }, + theme: {}, + }, + }); + const themeAddon = await AddonManager.installTemporaryAddon(themeXpi); + // Leave it pending uninstall, the following assertions related to + // the pending uninstall message bars will fail if the theme is listed. + await themeAddon.uninstall(true); + + // Install a third addon to verify that is being fully removed once the + // about:addons page is closed. + const xpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + name: "Test extension 3", + browser_specific_settings: { gecko: { id: "test-3@mochi.test" } }, + icons: { + 32: "test-icon.png", + }, + }, + }); + + added = BrowserTestUtils.waitForEvent(list, "add"); + const addon3 = await AddonManager.installTemporaryAddon(xpi); + await added; + ok( + getCardByAddonId(list, addon3.id), + "Addon card for the third extension added" + ); + + removed = BrowserTestUtils.waitForEvent(list, "remove"); + addon3.uninstall(true); + await removed; + ok( + !getCardByAddonId(list, addon3.id), + "Addon card for the third extension removed" + ); + + assertHasPendingUninstalls(list, 1); + ok( + addon3 && !!(addon3.pendingOperations & AddonManager.PENDING_UNINSTALL), + "The third addon is pending uninstall" + ); + + await closeView(win); + + ok( + !(await AddonManager.getAddonByID(addon3.id)), + "The third addon has been fully uninstalled" + ); + + ok( + themeAddon.pendingOperations & AddonManager.PENDING_UNINSTALL, + "The theme addon is pending after the list extension view is closed" + ); + + await themeAddon.uninstall(); + + ok( + !(await AddonManager.getAddonByID(themeAddon.id)), + "The theme addon is fully uninstalled" + ); +}); + +add_task(async function testMouseSupport() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test extension", + browser_specific_settings: { gecko: { id: "test@mochi.test" } }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + let doc = win.document; + + let [card] = getTestCards(doc); + is(card.addon.id, "test@mochi.test", "The right card is found"); + + let panel = card.querySelector("panel-list"); + + ok(!panel.open, "The panel is initially closed"); + await BrowserTestUtils.synthesizeMouseAtCenter( + "addon-card[addon-id$='@mochi.test'] button[action='more-options']", + { type: "mousedown" }, + win.docShell.browsingContext + ); + ok(panel.open, "The panel is now open"); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testKeyboardSupport() { + let id = "test@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test extension", + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + let doc = win.document; + + // Some helpers. + let tab = event => EventUtils.synthesizeKey("VK_TAB", event); + let space = () => EventUtils.synthesizeKey(" ", {}); + let isFocused = (el, msg) => is(doc.activeElement, el, msg); + + // Find the addon-list to listen for events. + let list = doc.querySelector("addon-list"); + let enabledSection = getSection(doc, "extension-enabled-section"); + let disabledSection = getSection(doc, "extension-disabled-section"); + + // Find the card. + let [card] = getTestCards(list); + is(card.addon.id, "test@mochi.test", "The right card is found"); + + // Focus the more options menu button. + let moreOptionsButton = card.querySelector('[action="more-options"]'); + moreOptionsButton.focus(); + isFocused(moreOptionsButton, "The more options button is focused"); + + // Test opening and closing the menu. + let moreOptionsMenu = card.querySelector("panel-list"); + let expandButton = moreOptionsMenu.querySelector('[action="expand"]'); + let removeButton = card.querySelector('[action="remove"]'); + is(moreOptionsMenu.open, false, "The menu is closed"); + let shown = BrowserTestUtils.waitForEvent(moreOptionsMenu, "shown"); + space(); + await shown; + is(moreOptionsMenu.open, true, "The menu is open"); + isFocused(removeButton, "The remove button is now focused"); + tab({ shiftKey: true }); + is(moreOptionsMenu.open, true, "The menu stays open"); + isFocused(expandButton, "The focus has looped to the bottom"); + tab(); + is(moreOptionsMenu.open, true, "The menu stays open"); + isFocused(removeButton, "The focus has looped to the top"); + + let hidden = BrowserTestUtils.waitForEvent(moreOptionsMenu, "hidden"); + EventUtils.synthesizeKey("Escape", {}); + await hidden; + isFocused(moreOptionsButton, "Escape closed the menu"); + + // Disable the add-on. + let disableButton = card.querySelector('[action="toggle-disabled"]'); + tab({ shiftKey: true }); + isFocused(disableButton, "The disable toggle is focused"); + is(card.parentNode, enabledSection, "The card is in the enabled section"); + space(); + // Wait for the add-on state to change. + let [disabledAddon] = await AddonTestUtils.promiseAddonEvent("onDisabled"); + is(disabledAddon.id, id, "The right add-on was disabled"); + is( + card.parentNode, + enabledSection, + "The card is still in the enabled section" + ); + isFocused(disableButton, "The disable button is still focused"); + let moved = BrowserTestUtils.waitForEvent(list, "move"); + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a non-interactive element to clear the focused + // state with a mouse which can be done by assistive technology and keyboard + // by pressing `Esc`, this rule check shall be ignored by a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + // Click outside the list to clear any focus. + EventUtils.synthesizeMouseAtCenter( + doc.querySelector(".header-name"), + {}, + win + ); + AccessibilityUtils.resetEnv(); + await moved; + is( + card.parentNode, + disabledSection, + "The card moved when keyboard focus left the list" + ); + + // Remove the add-on. + moreOptionsButton.focus(); + shown = BrowserTestUtils.waitForEvent(moreOptionsMenu, "shown"); + space(); + is(moreOptionsMenu.open, true, "The menu is open"); + await shown; + isFocused(removeButton, "The remove button is focused"); + let removed = BrowserTestUtils.waitForEvent(list, "remove"); + space(); + await removed; + is(card.parentNode, null, "The card is no longer on the page"); + + await extension.unload(); + await closeView(win); +}); + +add_task(async function testOpenDetailFromNameKeyboard() { + let id = "details@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Detail extension", + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + + let card = getCardByAddonId(win.document, id); + + info("focus the add-on's name, which should be an <a>"); + card.addonNameEl.focus(); + + let detailsLoaded = waitForViewLoad(win); + EventUtils.synthesizeKey("KEY_Enter", {}, win); + await detailsLoaded; + + card = getCardByAddonId(win.document, id); + is( + card.addonNameEl.textContent, + "Detail extension", + "The right detail view is laoded" + ); + + await extension.unload(); + await closeView(win); +}); + +add_task(async function testExtensionReordering() { + let extensions = createExtensions([ + { name: "Extension One" }, + { name: "This is last" }, + { name: "An extension, is first" }, + ]); + + await Promise.all(extensions.map(extension => extension.startup())); + + let win = await loadInitialView("extension"); + let doc = win.document; + + // Get a reference to the addon-list for events. + let list = doc.querySelector("addon-list"); + + // Find the related cards, they should all have @mochi.test ids. + let enabledSection = getSection(doc, "extension-enabled-section"); + let cards = getTestCards(enabledSection); + + is(cards.length, 3, "Each extension has an addon-card"); + + let order = Array.from(cards).map(card => card.addon.name); + Assert.deepEqual( + order, + ["An extension, is first", "Extension One", "This is last"], + "The add-ons are sorted by name" + ); + + // Disable the second extension. + let disabledSection = getSection(doc, "extension-disabled-section"); + ok(isEmpty(disabledSection), "The disabled section is initially empty"); + + // Disable the add-ons in a different order. + let reorderedCards = [cards[1], cards[0], cards[2]]; + for (let { addon } of reorderedCards) { + let moved = BrowserTestUtils.waitForEvent(list, "move"); + await addon.disable(); + await moved; + } + + order = Array.from(getTestCards(disabledSection)).map( + card => card.addon.name + ); + Assert.deepEqual( + order, + ["An extension, is first", "Extension One", "This is last"], + "The add-ons are sorted by name" + ); + + // All of our installed add-ons are disabled, install a new one. + let [newExtension] = createExtensions([{ name: "Extension New" }]); + let added = BrowserTestUtils.waitForEvent(list, "add"); + await newExtension.startup(); + await added; + + let [newCard] = getTestCards(enabledSection); + is( + newCard.addon.name, + "Extension New", + "The new add-on is in the enabled list" + ); + + // Enable everything again. + for (let { addon } of cards) { + let moved = BrowserTestUtils.waitForEvent(list, "move"); + await addon.enable(); + await moved; + } + + order = Array.from(getTestCards(enabledSection)).map(card => card.addon.name); + Assert.deepEqual( + order, + [ + "An extension, is first", + "Extension New", + "Extension One", + "This is last", + ], + "The add-ons are sorted by name" + ); + + // Remove the new extension. + let removed = BrowserTestUtils.waitForEvent(list, "remove"); + await newExtension.unload(); + await removed; + is(newCard.parentNode, null, "The new card has been removed"); + + await Promise.all(extensions.map(extension => extension.unload())); + await closeView(win); +}); + +add_task(async function testThemeList() { + let theme = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id: "theme@mochi.test" } }, + name: "My theme", + theme: {}, + }, + useAddonManager: "temporary", + }); + + let win = await loadInitialView("theme"); + let doc = win.document; + + let list = doc.querySelector("addon-list"); + + let cards = getTestCards(list); + is(cards.length, 0, "There are no test themes to start"); + + let added = BrowserTestUtils.waitForEvent(list, "add"); + await theme.startup(); + await added; + + cards = getTestCards(list); + is(cards.length, 1, "There is now one custom theme"); + + let [card] = cards; + is(card.addon.name, "My theme", "The card is for the test theme"); + + let enabledSection = getSection(doc, "theme-enabled-section"); + let disabledSection = getSection(doc, "theme-disabled-section"); + + await TestUtils.waitForCondition( + () => enabledSection.querySelectorAll("addon-card").length == 1 + ); + + is( + card.parentNode, + enabledSection, + "The new theme card is in the enabled section" + ); + is( + enabledSection.querySelectorAll("addon-card").length, + 1, + "There is one enabled theme" + ); + + let toggleThemeEnabled = async () => { + let themesChanged = waitForThemeChange(list); + card.querySelector('[action="toggle-disabled"]').click(); + await themesChanged; + + await TestUtils.waitForCondition( + () => enabledSection.querySelectorAll("addon-card").length == 1 + ); + }; + + await toggleThemeEnabled(); + + is( + card.parentNode, + disabledSection, + "The card is now in the disabled section" + ); + is( + enabledSection.querySelectorAll("addon-card").length, + 1, + "There is one enabled theme" + ); + + // Re-enable the theme. + await toggleThemeEnabled(); + is(card.parentNode, enabledSection, "Card is back in the Enabled section"); + + // Remove theme and verify that the default theme is re-enabled. + let removed = BrowserTestUtils.waitForEvent(list, "remove"); + // Confirm removal. + promptService._response = 0; + card.querySelector('[action="remove"]').click(); + await removed; + is(card.parentNode, null, "Card has been removed from the view"); + await TestUtils.waitForCondition( + () => enabledSection.querySelectorAll("addon-card").length == 1 + ); + + let defaultTheme = getCardByAddonId(doc, "default-theme@mozilla.org"); + is(defaultTheme.parentNode, enabledSection, "The default theme is reenabled"); + + await testUndoPendingUninstall(list, card.addon); + await TestUtils.waitForCondition( + () => enabledSection.querySelectorAll("addon-card").length == 1 + ); + is(defaultTheme.parentNode, disabledSection, "The default theme is disabled"); + ok(getCardByAddonId(enabledSection, theme.id), "Theme should be reenabled"); + + await theme.unload(); + await closeView(win); +}); + +add_task(async function testBuiltInThemeButtons() { + let win = await loadInitialView("theme"); + let doc = win.document; + + // Find the addon-list to listen for events. + let list = doc.querySelector("addon-list"); + let enabledSection = getSection(doc, "theme-enabled-section"); + let disabledSection = getSection(doc, "theme-disabled-section"); + + let defaultTheme = getCardByAddonId(doc, "default-theme@mozilla.org"); + let darkTheme = getCardByAddonId(doc, "firefox-compact-dark@mozilla.org"); + + // Check that themes are in the expected spots. + is(defaultTheme.parentNode, enabledSection, "The default theme is enabled"); + is(darkTheme.parentNode, disabledSection, "The dark theme is disabled"); + + // The default theme shouldn't have remove or disable options. + let defaultButtons = { + toggleDisabled: defaultTheme.querySelector('[action="toggle-disabled"]'), + remove: defaultTheme.querySelector('[action="remove"]'), + }; + is(defaultButtons.toggleDisabled.hidden, true, "Disable is hidden"); + is(defaultButtons.remove.hidden, true, "Remove is hidden"); + + // The dark theme should have an enable button, but not remove. + let darkButtons = { + toggleDisabled: darkTheme.querySelector('[action="toggle-disabled"]'), + remove: darkTheme.querySelector('[action="remove"]'), + }; + is(darkButtons.toggleDisabled.hidden, false, "Enable is visible"); + is(darkButtons.remove.hidden, true, "Remove is hidden"); + + // Enable the dark theme and check the buttons again. + let themesChanged = waitForThemeChange(list); + darkButtons.toggleDisabled.click(); + await themesChanged; + + await TestUtils.waitForCondition( + () => enabledSection.querySelectorAll("addon-card").length == 1 + ); + + // Check the buttons. + is(defaultButtons.toggleDisabled.hidden, false, "Enable is visible"); + is(defaultButtons.remove.hidden, true, "Remove is hidden"); + is(darkButtons.toggleDisabled.hidden, false, "Disable is visible"); + is(darkButtons.remove.hidden, true, "Remove is hidden"); + + // Disable the dark theme. + themesChanged = waitForThemeChange(list); + darkButtons.toggleDisabled.click(); + await themesChanged; + + await TestUtils.waitForCondition( + () => enabledSection.querySelectorAll("addon-card").length == 1 + ); + + // The themes are back to their starting posititons. + is(defaultTheme.parentNode, enabledSection, "Default is enabled"); + is(darkTheme.parentNode, disabledSection, "Dark is disabled"); + + await closeView(win); +}); + +add_task(async function testSideloadRemoveButton() { + const id = "sideload@mochi.test"; + mockProvider.createAddons([ + { + id, + name: "Sideloaded", + permissions: 0, + }, + ]); + + let win = await loadInitialView("extension"); + let doc = win.document; + + let card = getCardByAddonId(doc, id); + + let moreOptionsPanel = card.querySelector("panel-list"); + let moreOptionsButton = card.querySelector('[action="more-options"]'); + let panelOpened = BrowserTestUtils.waitForEvent(moreOptionsPanel, "shown"); + EventUtils.synthesizeMouseAtCenter(moreOptionsButton, {}, win); + await panelOpened; + + // Verify the remove button is visible with a SUMO link. + let removeButton = card.querySelector('[action="remove"]'); + ok(removeButton.disabled, "Remove is disabled"); + ok(!removeButton.hidden, "Remove is visible"); + + // Remove but cancel. + let prevented = BrowserTestUtils.waitForEvent(card, "remove-disabled"); + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a disabled control to confirm the click event + // won't come through. It is not meant to be interactive and is not expected + // to be accessible, therefore the rule check shall be ignored by a11y_checks. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + removeButton.click(); + AccessibilityUtils.resetEnv(); + await prevented; + + // reopen the panel + panelOpened = BrowserTestUtils.waitForEvent(moreOptionsPanel, "shown"); + EventUtils.synthesizeMouseAtCenter(moreOptionsButton, {}, win); + await panelOpened; + + let sumoLink = removeButton.querySelector("a"); + ok(sumoLink, "There's a link"); + is( + doc.l10n.getAttributes(removeButton).id, + "remove-addon-disabled-button", + "The can't remove text is shown" + ); + sumoLink.focus(); + is(doc.activeElement, sumoLink, "The link can be focused"); + + let newTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, REMOVE_SUMO_URL); + sumoLink.click(); + BrowserTestUtils.removeTab(await newTabOpened); + + await closeView(win); +}); + +add_task(async function testOnlyTypeIsShown() { + let win = await loadInitialView("theme"); + let doc = win.document; + + // Find the addon-list to listen for events. + let list = doc.querySelector("addon-list"); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test extension", + browser_specific_settings: { gecko: { id: "test@mochi.test" } }, + }, + useAddonManager: "temporary", + }); + + let skipped = BrowserTestUtils.waitForEvent( + list, + "skip-add", + e => e.detail == "type-mismatch" + ); + await extension.startup(); + await skipped; + + let cards = getTestCards(list); + is(cards.length, 0, "There are no test extension cards"); + + await extension.unload(); + await closeView(win); +}); + +add_task(async function testPluginIcons() { + const pluginIconUrl = "chrome://global/skin/icons/plugin.svg"; + + let win = await loadInitialView("plugin"); + let doc = win.document; + + // Check that the icons are set to the plugin icon. + let icons = doc.querySelectorAll(".card-heading-icon"); + ok(!!icons.length, "There are some plugins listed"); + + for (let icon of icons) { + is(icon.src, pluginIconUrl, "Plugins use the plugin icon"); + } + + await closeView(win); +}); + +add_task(async function testExtensionGenericIcon() { + const extensionIconUrl = + "chrome://mozapps/skin/extensions/extensionGeneric.svg"; + + let id = "test@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test extension", + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + let doc = win.document; + + let card = getCardByAddonId(doc, id); + let icon = card.querySelector(".addon-icon"); + is(icon.src, extensionIconUrl, "Extensions without icon use the generic one"); + + await extension.unload(); + await closeView(win); +}); + +add_task(async function testSectionHeadingKeys() { + mockProvider.createAddons([ + { + id: "test-theme", + name: "Test Theme", + type: "theme", + }, + { + id: "test-extension-disabled", + name: "Test Disabled Extension", + type: "extension", + userDisabled: true, + }, + { + id: "test-plugin-disabled", + name: "Test Disabled Plugin", + type: "plugin", + userDisabled: true, + }, + { + id: "test-locale", + name: "Test Enabled Locale", + type: "locale", + }, + { + id: "test-locale-disabled", + name: "Test Disabled Locale", + type: "locale", + userDisabled: true, + }, + { + id: "test-dictionary", + name: "Test Enabled Dictionary", + type: "dictionary", + }, + { + id: "test-dictionary-disabled", + name: "Test Disabled Dictionary", + type: "dictionary", + userDisabled: true, + }, + { + id: "test-sitepermission", + name: "Test Enabled Site Permission", + type: "sitepermission", + }, + { + id: "test-sitepermission-disabled", + name: "Test Disabled Site Permission", + type: "sitepermission", + userDisabled: true, + }, + ]); + + for (let type of [ + "extension", + "theme", + "plugin", + "locale", + "dictionary", + "sitepermission", + ]) { + info(`loading view for addon type ${type}`); + let win = await loadInitialView(type); + let doc = win.document; + + for (let status of ["enabled", "disabled"]) { + let section = getSection(doc, `${type}-${status}-section`); + let el = section?.querySelector(".list-section-heading"); + isnot(el, null, `Should have ${status} heading for ${type} section`); + is( + el && doc.l10n.getAttributes(el).id, + win.getL10nIdMapping(`${type}-${status}-heading`), + `Should have correct ${status} heading for ${type} section` + ); + } + + await closeView(win); + } +}); + +add_task(async function testDisabledDimming() { + const id = "disabled@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Disable me", + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let addon = await AddonManager.getAddonByID(id); + + let win = await loadInitialView("extension"); + let doc = win.document; + let pageHeader = doc.querySelector("addon-page-header"); + + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a non-interactive element to clear the focused + // state with a mouse which can be done by assistive technology and keyboard + // by pressing `Esc`, this rule check shall be ignored by a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + // Ensure there's no focus on the list. + EventUtils.synthesizeMouseAtCenter(pageHeader, {}, win); + AccessibilityUtils.resetEnv(); + + const checkOpacity = (card, expected, msg) => { + let { opacity } = card.ownerGlobal.getComputedStyle(card.firstElementChild); + let normalize = val => Math.floor(val * 10); + is(normalize(opacity), normalize(expected), msg); + }; + const waitForTransition = card => + BrowserTestUtils.waitForEvent( + card.firstElementChild, + "transitionend", + /* capture = */ false, + e => e.propertyName === "opacity" && e.target.classList.contains("card") + ); + + let card = getCardByAddonId(doc, id); + checkOpacity(card, "1", "The opacity is 1 when enabled"); + + // Disable the add-on, check again. + let list = doc.querySelector("addon-list"); + let moved = BrowserTestUtils.waitForEvent(list, "move"); + await addon.disable(); + await moved; + + let disabledSection = getSection(doc, "extension-disabled-section"); + is(card.parentNode, disabledSection, "The card is in the disabled section"); + checkOpacity(card, "0.6", "The opacity is dimmed when disabled"); + + // Click on the menu button, this should un-dim the card. + let transitionEnded = waitForTransition(card); + let moreOptionsButton = card.querySelector(".more-options-button"); + EventUtils.synthesizeMouseAtCenter(moreOptionsButton, {}, win); + await transitionEnded; + checkOpacity(card, "1", "The opacity is 1 when the menu is open"); + + // Close the menu, opacity should return. + transitionEnded = waitForTransition(card); + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a non-interactive element to dismiss the opened + // menu with a mouse which can be done by assistive technology and keyboard + // by pressing `Esc`, this rule check shall be ignored by a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + EventUtils.synthesizeMouseAtCenter(pageHeader, {}, win); + AccessibilityUtils.resetEnv(); + await transitionEnded; + checkOpacity(card, "0.6", "The card is dimmed again"); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testEmptyMessage() { + let tests = [ + { + type: "extension", + message: "Get extensions and themes on ", + }, + { + type: "theme", + message: "Get extensions and themes on ", + }, + { + type: "plugin", + message: "Get extensions and themes on ", + }, + { + type: "locale", + message: "Get language packs on ", + }, + { + type: "dictionary", + message: "Get dictionaries on ", + }, + ]; + + for (let test of tests) { + let win = await loadInitialView(test.type); + let doc = win.document; + let enabledSection = getSection(doc, `${test.type}-enabled-section`); + let disabledSection = getSection(doc, `${test.type}-disabled-section`); + const message = doc.querySelector("#empty-addons-message"); + + // Test if the correct locale has been applied. + ok( + message.textContent.startsWith(test.message), + `View ${test.type} has correct empty list message` + ); + + // With at least one enabled/disabled add-on (see testSectionHeadingKeys), + // the message is hidden. + is_element_hidden(message, "Empty addons message hidden"); + + // The test runner (Mochitest) relies on add-ons that should not be removed. + // Simulate the scenario of zero add-ons by clearing all rendered sections. + while (enabledSection.firstChild) { + enabledSection.firstChild.remove(); + } + + while (disabledSection.firstChild) { + disabledSection.firstChild.remove(); + } + + // Message should now be displayed + is_element_visible(message, "Empty addons message visible"); + + await closeView(win); + } +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_list_view_recommendations.js b/toolkit/mozapps/extensions/test/browser/browser_html_list_view_recommendations.js new file mode 100644 index 0000000000..db4067ab35 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_list_view_recommendations.js @@ -0,0 +1,293 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint max-len: ["error", 80] */ +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +function makeResult({ guid, type }) { + return { + addon: { + authors: [{ name: "Some author" }], + current_version: { + files: [{ platform: "all", url: "data:," }], + }, + url: "data:,", + guid, + type, + }, + }; +} + +function mockResults() { + let types = ["extension", "theme", "extension", "extension", "theme"]; + return { + results: types.map((type, i) => + makeResult({ + guid: `${type}${i}@mochi.test`, + type, + }) + ), + }; +} + +add_setup(async function () { + let results = btoa(JSON.stringify(mockResults())); + await SpecialPowers.pushPrefEnv({ + set: [ + // Disable personalized recommendations, they will break the data URI. + ["browser.discovery.enabled", false], + ["extensions.getAddons.discovery.api_url", `data:;base64,${results}`], + [ + "extensions.recommendations.themeRecommendationUrl", + "https://example.com/theme", + ], + ], + }); +}); + +function checkExtraContents(doc, type, opts = {}) { + let { showThemeRecommendationFooter = type === "theme" } = opts; + let footer = doc.querySelector("footer"); + let amoButton = footer.querySelector('[action="open-amo"]'); + let privacyPolicyLink = footer.querySelector(".privacy-policy-link"); + let themeRecommendationFooter = footer.querySelector(".theme-recommendation"); + let themeRecommendationLink = + themeRecommendationFooter && themeRecommendationFooter.querySelector("a"); + let taarNotice = doc.querySelector("taar-notice"); + + is_element_visible(footer, "The footer is visible"); + + if (type == "extension") { + ok(taarNotice, "There is a TAAR notice"); + is_element_visible(amoButton, "The AMO button is shown"); + is_element_visible(privacyPolicyLink, "The privacy policy is visible"); + } else if (type == "theme") { + ok(!taarNotice, "There is no TAAR notice"); + ok(amoButton, "AMO button is shown"); + ok(!privacyPolicyLink, "There is no privacy policy"); + } else { + throw new Error(`Unknown type ${type}`); + } + + if (showThemeRecommendationFooter) { + is_element_visible( + themeRecommendationFooter, + "There's a theme recommendation footer" + ); + is_element_visible(themeRecommendationLink, "There's a link to the theme"); + is(themeRecommendationLink.target, "_blank", "The link opens in a new tab"); + is( + themeRecommendationLink.href, + "https://example.com/theme", + "The link goes to the pref's URL" + ); + is( + doc.l10n.getAttributes(themeRecommendationFooter).id, + "recommended-theme-1", + "The recommendation has the right l10n-id" + ); + } else { + ok( + !themeRecommendationFooter || themeRecommendationFooter.hidden, + "There's no theme recommendation" + ); + } +} + +async function installAddon({ card, recommendedList, manifestExtra = {} }) { + // Install an add-on to hide the card. + let hidden = BrowserTestUtils.waitForEvent( + recommendedList, + "card-hidden", + false, + e => e.detail.card == card + ); + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id: card.addonId } }, + ...manifestExtra, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + await hidden; + return extension; +} + +async function testListRecommendations({ type, manifestExtra = {} }) { + let win = await loadInitialView(type); + let doc = win.document; + + // Wait for the list to render, rendering is tested with the discovery pane. + let recommendedList = doc.querySelector("recommended-addon-list"); + await recommendedList.cardsReady; + + checkExtraContents(doc, type); + + // Check that the cards are all for the right type. + let cards = doc.querySelectorAll("recommended-addon-card"); + ok(!!cards.length, "There were some cards found"); + for (let card of cards) { + is(card.discoAddon.type, type, `The card is for a ${type}`); + is_element_visible(card, "The card is visible"); + } + + // Install an add-on for the first card, verify it is hidden. + let { addonId } = cards[0]; + ok(addonId, "The card has an addonId"); + + // Installing the add-on will fail since the URL doesn't point to a valid + // XPI. + let installButton = cards[0].querySelector('[action="install-addon"]'); + let { panel } = PopupNotifications; + let popupId = "addon-install-failed-notification"; + let failPromise = TestUtils.topicObserved("addon-install-failed"); + installButton.click(); + await failPromise; + // Wait for the installing popup to be hidden and leave just the error popup. + await BrowserTestUtils.waitForCondition(() => { + return panel.children.length == 1 && panel.firstElementChild.id == popupId; + }); + + // Dismiss the popup. + panel.firstElementChild.button.click(); + await BrowserTestUtils.waitForPopupEvent(panel, "hidden"); + + let extension = await installAddon({ card: cards[0], recommendedList }); + is_element_hidden(cards[0], "The card is now hidden"); + + // Switch away and back, there should still be a hidden card. + await closeView(win); + win = await loadInitialView(type); + doc = win.document; + recommendedList = doc.querySelector("recommended-addon-list"); + await recommendedList.cardsReady; + + cards = Array.from(doc.querySelectorAll("recommended-addon-card")); + + let hiddenCard = cards.pop(); + is(hiddenCard.addonId, addonId, "The expected card was found"); + is_element_hidden(hiddenCard, "The card is still hidden"); + + ok(!!cards.length, "There are still some visible cards"); + for (let card of cards) { + is(card.discoAddon.type, type, `The card is for a ${type}`); + is_element_visible(card, "The card is visible"); + } + + // Uninstall the add-on, verify the card is shown again. + let shown = BrowserTestUtils.waitForEvent(recommendedList, "card-shown"); + await extension.unload(); + await shown; + + is_element_visible(hiddenCard, "The card is now shown"); + + await closeView(win); +} + +add_task(async function testExtensionList() { + await testListRecommendations({ type: "extension" }); +}); + +add_task(async function testThemeList() { + await testListRecommendations({ + type: "theme", + manifestExtra: { theme: {} }, + }); +}); + +add_task(async function testInstallAllExtensions() { + let type = "extension"; + let win = await loadInitialView(type); + let doc = win.document; + + // Wait for the list to render, rendering is tested with the discovery pane. + let recommendedList = doc.querySelector("recommended-addon-list"); + await recommendedList.cardsReady; + + // Find more button is shown. + checkExtraContents(doc, type); + + let cards = Array.from(doc.querySelectorAll("recommended-addon-card")); + is(cards.length, 3, "We found some cards"); + + let extensions = await Promise.all( + cards.map(card => installAddon({ card, recommendedList })) + ); + + // The find more on AMO button is shown. + checkExtraContents(doc, type); + + // Uninstall one of the extensions, the button should still be shown. + let extension = extensions.pop(); + let shown = BrowserTestUtils.waitForEvent(recommendedList, "card-shown"); + await extension.unload(); + await shown; + + // The find more on AMO button is shown. + checkExtraContents(doc, type); + + await Promise.all(extensions.map(extension => extension.unload())); + await closeView(win); +}); + +add_task(async function testError() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.getAddons.discovery.api_url", "data:,"]], + }); + + let win = await loadInitialView("extension"); + let doc = win.document; + + // Wait for the list to render, rendering is tested with the discovery pane. + let recommendedList = doc.querySelector("recommended-addon-list"); + await recommendedList.cardsReady; + + checkExtraContents(doc, "extension"); + + await closeView(win); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function testThemesNoRecommendationUrl() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.recommendations.themeRecommendationUrl", ""]], + }); + + let win = await loadInitialView("theme"); + let doc = win.document; + + // Wait for the list to render, rendering is tested with the discovery pane. + let recommendedList = doc.querySelector("recommended-addon-list"); + await recommendedList.cardsReady; + + checkExtraContents(doc, "theme", { showThemeRecommendationFooter: false }); + + await closeView(win); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function testRecommendationsDisabled() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.htmlaboutaddons.recommendations.enabled", false]], + }); + + let types = ["extension", "theme"]; + + for (let type of types) { + let win = await loadInitialView(type); + let doc = win.document; + + let recommendedList = doc.querySelector("recommended-addon-list"); + ok(!recommendedList, `There are no recommendations on the ${type} page`); + + await closeView(win); + } + + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_message_bar.js b/toolkit/mozapps/extensions/test/browser/browser_html_message_bar.js new file mode 100644 index 0000000000..b60baf8799 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_message_bar.js @@ -0,0 +1,185 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint max-len: ["error", 80] */ + +let htmlAboutAddonsWindow; + +const HTML_NS = "http://www.w3.org/1999/xhtml"; + +function clickElement(el) { + el.dispatchEvent(new CustomEvent("click")); +} + +function createMessageBar(messageBarStack, { attrs, children, onclose } = {}) { + const win = messageBarStack.ownerGlobal; + const messageBar = win.document.createElementNS(HTML_NS, "message-bar"); + if (attrs) { + for (const [k, v] of Object.entries(attrs)) { + messageBar.setAttribute(k, v); + } + } + if (children) { + if (Array.isArray(children)) { + messageBar.append(...children); + } else { + messageBar.append(children); + } + } + messageBar.addEventListener("message-bar:close", onclose, { once: true }); + messageBarStack.append(messageBar); + return messageBar; +} + +add_setup(async function () { + htmlAboutAddonsWindow = await loadInitialView("extension"); + registerCleanupFunction(() => closeView(htmlAboutAddonsWindow)); +}); + +add_task(async function test_message_bar_stack() { + const win = htmlAboutAddonsWindow; + + let messageBarStack = win.document.getElementById("abuse-reports-messages"); + + ok(messageBarStack, "Got a message-bar-stack in HTML about:addons page"); + + is( + messageBarStack.maxMessageBarCount, + 3, + "Got the expected max-message-bar-count property" + ); + + is( + messageBarStack.childElementCount, + 0, + "message-bar-stack is initially empty" + ); +}); + +add_task(async function test_create_message_bar_create_and_onclose() { + const win = htmlAboutAddonsWindow; + const messageBarStack = win.document.getElementById("abuse-reports-messages"); + + let messageEl = win.document.createElementNS(HTML_NS, "span"); + messageEl.textContent = "A message bar text"; + let buttonEl = win.document.createElementNS(HTML_NS, "button"); + buttonEl.textContent = "An action button"; + + let messageBar; + let onceMessageBarClosed = new Promise(resolve => { + messageBar = createMessageBar(messageBarStack, { + children: [messageEl, buttonEl], + onclose: resolve, + }); + }); + + is( + messageBarStack.childElementCount, + 1, + "message-bar-stack has a child element" + ); + is( + messageBarStack.firstElementChild, + messageBar, + "newly created message-bar added as message-bar-stack child element" + ); + + const slot = messageBar.shadowRoot.querySelector("slot"); + is( + slot.assignedNodes()[0], + messageEl, + "Got the expected span element assigned to the message-bar slot" + ); + is( + slot.assignedNodes()[1], + buttonEl, + "Got the expected button element assigned to the message-bar slot" + ); + + let dismissed = BrowserTestUtils.waitForEvent( + messageBar, + "message-bar:user-dismissed" + ); + info("Click the close icon on the newly created message-bar"); + clickElement(messageBar.closeButton); + await dismissed; + + info("Expect the onclose function to be called"); + await onceMessageBarClosed; + + is( + messageBarStack.childElementCount, + 0, + "message-bar-stack has no child elements" + ); +}); + +add_task(async function test_max_message_bar_count() { + const win = htmlAboutAddonsWindow; + const messageBarStack = win.document.getElementById("abuse-reports-messages"); + + info("Create a new message-bar"); + let messageElement = document.createElementNS(HTML_NS, "span"); + messageElement = "message bar label"; + + let onceMessageBarClosed = new Promise(resolve => { + createMessageBar(messageBarStack, { + children: messageElement, + onclose: resolve, + }); + }); + + is( + messageBarStack.childElementCount, + 1, + "message-bar-stack has the expected number of children" + ); + + info("Create 3 more message bars"); + const allBarsPromises = []; + for (let i = 2; i <= 4; i++) { + allBarsPromises.push( + new Promise(resolve => { + createMessageBar(messageBarStack, { + attrs: { dismissable: "" }, + children: [messageElement, i], + onclose: resolve, + }); + }) + ); + } + + info("Expect first message-bar to closed automatically"); + await onceMessageBarClosed; + + is( + messageBarStack.childElementCount, + 3, + "message-bar-stack has the expected number of children" + ); + + info("Click on close icon for the second message-bar"); + clickElement(messageBarStack.firstElementChild.closeButton); + + info("Expect the second message-bar to be closed"); + await allBarsPromises[0]; + + is( + messageBarStack.childElementCount, + 2, + "message-bar-stack has the expected number of children" + ); + + info("Clear the entire message-bar-stack content"); + messageBarStack.textContent = ""; + + info("Expect all the created message-bar to be closed automatically"); + await Promise.all(allBarsPromises); + + is( + messageBarStack.childElementCount, + 0, + "message-bar-stack has no child elements" + ); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_options_ui.js b/toolkit/mozapps/extensions/test/browser/browser_html_options_ui.js new file mode 100644 index 0000000000..c5bfa1022f --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_options_ui.js @@ -0,0 +1,651 @@ +/* eslint max-len: ["error", 80] */ + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { ExtensionParent } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionParent.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +// This test function helps to detect when an addon options browser have been +// inserted in the about:addons page. +function waitOptionsBrowserInserted() { + return new Promise(resolve => { + async function listener(eventName, browser) { + // wait for a webextension XUL browser element that is owned by the + // "about:addons" page. + if (browser.ownerGlobal.top.location.href == "about:addons") { + ExtensionParent.apiManager.off("extension-browser-inserted", listener); + resolve(browser); + } + } + ExtensionParent.apiManager.on("extension-browser-inserted", listener); + }); +} + +add_task(async function enableHtmlViews() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.htmlaboutaddons.inline-options.enabled", true]], + }); +}); + +add_task(async function testInlineOptions() { + const HEIGHT_SHORT = 300; + const HEIGHT_TALL = 600; + + let id = "inline@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id } }, + options_ui: { + page: "options.html", + }, + }, + files: { + "options.html": ` + <html> + <head> + <style type="text/css"> + body > p { height: ${HEIGHT_SHORT}px; margin: 0; } + body.bigger > p { height: ${HEIGHT_TALL}px; } + </style> + <script src="options.js"></script> + </head> + <body> + <p>Some text</p> + </body> + </html> + `, + "options.js": () => { + browser.test.onMessage.addListener(msg => { + if (msg == "toggle-class") { + document.body.classList.toggle("bigger"); + } else if (msg == "get-height") { + browser.test.sendMessage("height", document.body.clientHeight); + } + }); + + browser.test.sendMessage("options-loaded", window.location.href); + }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + let doc = win.document; + + // Make sure we found the right card. + let card = getAddonCard(win, id); + ok(card, "Found the card"); + + // The preferences option should be visible. + let preferences = card.querySelector('[action="preferences"]'); + ok(!preferences.hidden, "The preferences option is visible"); + + // Open the preferences page. + let loaded = waitForViewLoad(win); + preferences.click(); + await loaded; + + // Verify we're on the preferences tab. + card = doc.querySelector("addon-card"); + is(card.addon.id, id, "The right page was loaded"); + let { deck, tabGroup } = card.details; + let { selectedViewName } = deck; + is(selectedViewName, "preferences", "The preferences tab is shown"); + + info("Check that there are two buttons and they're visible"); + let detailsBtn = tabGroup.querySelector('[name="details"]'); + ok(!detailsBtn.hidden, "The details button is visible"); + let prefsBtn = tabGroup.querySelector('[name="preferences"]'); + ok(!prefsBtn.hidden, "The preferences button is visible"); + + // Wait for the browser to load. + let url = await extension.awaitMessage("options-loaded"); + + // Check the attributes of the options browser. + let browser = card.querySelector("inline-options-browser browser"); + ok(browser, "The visible view has a browser"); + is( + browser.currentURI.spec, + card.addon.optionsURL, + "The browser has the expected options URL" + ); + is(url, card.addon.optionsURL, "Browser has the expected options URL loaded"); + let stack = browser.closest("stack"); + is( + browser.clientWidth, + stack.clientWidth, + "Browser should be the same width as its direct parent" + ); + Assert.greater(stack.clientWidth, 0, "The stack has a width"); + ok( + card.querySelector('[action="preferences"]').hidden, + "The preferences option is hidden now" + ); + + let waitForHeightChange = expectedHeight => + TestUtils.waitForCondition(() => browser.clientHeight === expectedHeight); + + await waitForHeightChange(HEIGHT_SHORT); + + // Check resizing the browser through extension CSS. + await extension.sendMessage("get-height"); + let height = await extension.awaitMessage("height"); + is(height, HEIGHT_SHORT, "The height is smaller to start"); + is(height, browser.clientHeight, "The browser is the same size"); + + info("Resize the browser to be taller"); + await extension.sendMessage("toggle-class"); + await waitForHeightChange(HEIGHT_TALL); + await extension.sendMessage("get-height"); + height = await extension.awaitMessage("height"); + is(height, HEIGHT_TALL, "The height is bigger now"); + is(height, browser.clientHeight, "The browser is the same size"); + + info("Shrink the browser again"); + await extension.sendMessage("toggle-class"); + await waitForHeightChange(HEIGHT_SHORT); + await extension.sendMessage("get-height"); + height = await extension.awaitMessage("height"); + is(height, HEIGHT_SHORT, "The browser shrunk back"); + is(height, browser.clientHeight, "The browser is the same size"); + + info("Switching to details view"); + detailsBtn.click(); + + info("Check the browser dimensions to make sure it's hidden"); + is(browser.clientWidth, 0, "The browser is hidden now"); + + info("Switch back, check browser is shown"); + prefsBtn.click(); + + is(browser.clientWidth, stack.clientWidth, "The browser width is set again"); + Assert.greater(stack.clientWidth, 0, "The stack has a width"); + + await closeView(win); + await extension.unload(); +}); + +// Regression test against bug 1409697 +add_task(async function testCardRerender() { + let id = "rerender@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id } }, + options_ui: { + page: "options.html", + }, + }, + files: { + "options.html": ` + <html> + <body> + <p>Some text</p> + </body> + </html> + `, + }, + useAddonManager: "permanent", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + let doc = win.document; + + let card = getAddonCard(win, id); + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + card = doc.querySelector("addon-card"); + + let browserAdded = waitOptionsBrowserInserted(); + card.querySelector('.tab-button[name="preferences"]').click(); + await browserAdded; + + is( + doc.querySelectorAll("inline-options-browser").length, + 1, + "There is 1 inline-options-browser" + ); + is(doc.querySelectorAll("browser").length, 1, "There is 1 browser"); + + info("Reload the add-on and ensure there's still only one browser"); + let updated = BrowserTestUtils.waitForEvent(card, "update"); + card.addon.reload(); + await updated; + + // Since the add-on was disabled, we'll be on the details tab. + is(card.details.deck.selectedViewName, "details", "View changed to details"); + is( + doc.querySelectorAll("inline-options-browser").length, + 1, + "There is 1 inline-options-browser" + ); + is(doc.querySelectorAll("browser").length, 0, "The browser was destroyed"); + + // Load the permissions tab again. + browserAdded = waitOptionsBrowserInserted(); + card.querySelector('.tab-button[name="preferences"]').click(); + await browserAdded; + + // Switching to preferences will create a new browser element. + is( + card.details.deck.selectedViewName, + "preferences", + "View switched to preferences" + ); + is( + doc.querySelectorAll("inline-options-browser").length, + 1, + "There is 1 inline-options-browser" + ); + is(doc.querySelectorAll("browser").length, 1, "There is a new browser"); + + info("Re-rendering card to ensure a second browser isn't added"); + updated = BrowserTestUtils.waitForEvent(card, "update"); + card.render(); + await updated; + + is( + card.details.deck.selectedViewName, + "details", + "Rendering reverted to the details view" + ); + is( + doc.querySelectorAll("inline-options-browser").length, + 1, + "There is still only 1 inline-options-browser after re-render" + ); + is(doc.querySelectorAll("browser").length, 0, "There is no browser"); + + let newBrowserAdded = waitOptionsBrowserInserted(); + card.showPrefs(); + await newBrowserAdded; + + is( + doc.querySelectorAll("inline-options-browser").length, + 1, + "There is still only 1 inline-options-browser after opening preferences" + ); + is(doc.querySelectorAll("browser").length, 1, "There is 1 browser"); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testRemovedOnDisable() { + let id = "disable@mochi.test"; + const xpiFile = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { gecko: { id } }, + options_ui: { + page: "options.html", + }, + }, + files: { + "options.html": "<h1>Options!</h1>", + }, + }); + let addon = await AddonManager.installTemporaryAddon(xpiFile); + + let win = await loadInitialView("extension"); + let doc = win.document; + + // Opens the prefs page. + let loaded = waitForViewLoad(win); + getAddonCard(win, id).querySelector("[action=preferences]").click(); + await loaded; + + let inlineOptions = doc.querySelector("inline-options-browser"); + ok(inlineOptions, "There's an inline-options-browser element"); + ok(inlineOptions.querySelector("browser"), "The browser exists"); + + let card = getAddonCard(win, id); + let { deck } = card.details; + is(deck.selectedViewName, "preferences", "Preferences are the active tab"); + + info("Disabling the add-on"); + let updated = BrowserTestUtils.waitForEvent(card, "update"); + await addon.disable(); + await updated; + + is(deck.selectedViewName, "details", "Details are now the active tab"); + ok(inlineOptions, "There's an inline-options-browser element"); + ok(!inlineOptions.querySelector("browser"), "The browser has been removed"); + + info("Enabling the add-on"); + updated = BrowserTestUtils.waitForEvent(card, "update"); + await addon.enable(); + await updated; + + is(deck.selectedViewName, "details", "Details are still the active tab"); + ok(inlineOptions, "There's an inline-options-browser element"); + ok(!inlineOptions.querySelector("browser"), "The browser is not created yet"); + + info("Switching to preferences tab"); + let changed = BrowserTestUtils.waitForEvent(deck, "view-changed"); + let browserAdded = waitOptionsBrowserInserted(); + deck.selectedViewName = "preferences"; + await changed; + await browserAdded; + + is(deck.selectedViewName, "preferences", "Preferences are selected"); + ok(inlineOptions, "There's an inline-options-browser element"); + ok(inlineOptions.querySelector("browser"), "The browser is re-created"); + + await closeView(win); + await addon.uninstall(); +}); + +add_task(async function testUpgradeTemporary() { + let id = "upgrade-temporary@mochi.test"; + async function loadExtension(version) { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id } }, + version, + options_ui: { + page: "options.html", + }, + }, + files: { + "options.html": ` + <html> + <head> + <script src="options.js"></script> + </head> + <body> + <p>Version <pre>${version}</pre></p> + </body> + </html> + `, + "options.js": () => { + browser.test.onMessage.addListener(msg => { + if (msg === "get-version") { + let version = document.querySelector("pre").textContent; + browser.test.sendMessage("version", version); + } + }); + window.onload = () => browser.test.sendMessage("options-loaded"); + }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + return extension; + } + + let firstExtension = await loadExtension("1"); + let win = await loadInitialView("extension"); + let doc = win.document; + + let card = getAddonCard(win, id); + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + card = doc.querySelector("addon-card"); + let browserAdded = waitOptionsBrowserInserted(); + card.querySelector('.tab-button[name="preferences"]').click(); + await browserAdded; + + await firstExtension.awaitMessage("options-loaded"); + await firstExtension.sendMessage("get-version"); + let version = await firstExtension.awaitMessage("version"); + is(version, "1", "Version 1 page is loaded"); + + let updated = BrowserTestUtils.waitForEvent(card, "update"); + browserAdded = waitOptionsBrowserInserted(); + let secondExtension = await loadExtension("2"); + await updated; + await browserAdded; + await secondExtension.awaitMessage("options-loaded"); + + await secondExtension.sendMessage("get-version"); + version = await secondExtension.awaitMessage("version"); + is(version, "2", "Version 2 page is loaded"); + let { deck } = card.details; + is(deck.selectedViewName, "preferences", "Preferences are still shown"); + + await closeView(win); + await firstExtension.unload(); + await secondExtension.unload(); +}); + +add_task(async function testReloadExtension() { + let id = "reload@mochi.test"; + let xpiFile = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { gecko: { id } }, + options_ui: { + page: "options.html", + }, + }, + files: { + "options.html": ` + <html> + <head> + </head> + <body> + <p>Options</p> + </body> + </html> + `, + }, + }); + let addon = await AddonManager.installTemporaryAddon(xpiFile); + + let win = await loadInitialView("extension"); + let doc = win.document; + + let card = getAddonCard(win, id); + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + card = doc.querySelector("addon-card"); + let { deck } = card.details; + is(deck.selectedViewName, "details", "Details load first"); + + let browserAdded = waitOptionsBrowserInserted(); + card.querySelector('.tab-button[name="preferences"]').click(); + await browserAdded; + + is(deck.selectedViewName, "preferences", "Preferences are shown"); + + let updated = BrowserTestUtils.waitForEvent(card, "update"); + browserAdded = waitOptionsBrowserInserted(); + let addonStarted = AddonTestUtils.promiseWebExtensionStartup(id); + await addon.reload(); + await addonStarted; + await updated; + await browserAdded; + is(deck.selectedViewName, "preferences", "Preferences are still shown"); + + await closeView(win); + await addon.uninstall(); +}); + +async function testSelectPosition(optionsBrowser, zoom) { + let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(window); + await BrowserTestUtils.synthesizeMouseAtCenter("select", {}, optionsBrowser); + let popup = await popupShownPromise; + let popupLeft = popup.shadowRoot.querySelector( + ".menupopup-arrowscrollbox" + ).screenX; + let browserLeft = optionsBrowser.screenX * zoom; + Assert.lessOrEqual( + Math.abs(popupLeft - browserLeft), + 1, + `Popup should be correctly positioned: ${popupLeft} vs. ${browserLeft}` + ); + popup.hidePopup(); +} + +async function testOptionsZoom(type = "full") { + let id = `${type}-zoom@mochi.test`; + let zoomProp = `${type}Zoom`; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id } }, + options_ui: { + page: "options.html", + }, + }, + files: { + "options.html": ` + <!doctype html> + <script src="options.js"></script> + <body style="height: 500px"> + <p>Some text</p> + <p> + <select> + <option>A</option> + <option>B</option> + </select> + </p> + </body> + `, + "options.js": () => { + window.addEventListener("load", function () { + browser.test.sendMessage("options-loaded"); + }); + }, + }, + useAddonManager: "permanent", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + let doc = win.document; + + gBrowser.selectedBrowser[zoomProp] = 2; + + let card = getAddonCard(win, id); + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + card = doc.querySelector("addon-card"); + + let browserAdded = waitOptionsBrowserInserted(); + card.querySelector('.tab-button[name="preferences"]').click(); + let optionsBrowser = await browserAdded; + // Wait for the browser to load. + await extension.awaitMessage("options-loaded"); + + is(optionsBrowser[zoomProp], 2, `Options browser inherited ${zoomProp}`); + + await testSelectPosition(optionsBrowser, type == "full" ? 2 : 1); + + gBrowser.selectedBrowser[zoomProp] = 0.5; + + is( + optionsBrowser[zoomProp], + 0.5, + `Options browser reacts to ${zoomProp} change` + ); + + await closeView(win); + await extension.unload(); +} + +add_task(function testOptionsFullZoom() { + return testOptionsZoom("full"); +}); + +add_task(function testOptionsTextZoom() { + return testOptionsZoom("text"); +}); + +add_task(async function testInputAndQuickFind() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + options_ui: { + page: "options.html", + }, + }, + files: { + "options.html": ` + <html> + <body> + <input name="some-input" type="text"> + <script src="options.js"></script> + </body> + </html> + `, + "options.js": () => { + let input = document.querySelector("input"); + browser.test.assertEq( + "some-input", + input.getAttribute("name"), + "Expected options page input" + ); + input.addEventListener("input", event => { + browser.test.sendMessage("input-changed", event.target.value); + }); + + browser.test.sendMessage("options-loaded", window.location.href); + }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + let doc = win.document; + + // Make sure we found the right card. + let card = getAddonCard(win, extension.id); + ok(card, "Found the card"); + + // The preferences option should be visible. + let preferences = card.querySelector('[action="preferences"]'); + ok(!preferences.hidden, "The preferences option is visible"); + + // Open the preferences page. + let loaded = waitForViewLoad(win); + preferences.click(); + await loaded; + + // Verify we're on the preferences tab. + card = doc.querySelector("addon-card"); + is(card.addon.id, extension.id, "The right page was loaded"); + + // Wait for the browser to load. + let url = await extension.awaitMessage("options-loaded"); + + // Check the attributes of the options browser. + let browser = card.querySelector("inline-options-browser browser"); + ok(browser, "The visible view has a browser"); + ok(card.addon.optionsURL.length, "Options URL is not empty"); + is( + browser.currentURI.spec, + card.addon.optionsURL, + "The browser has the expected options URL" + ); + is(url, card.addon.optionsURL, "Browser has the expected options URL loaded"); + + // Focus the options browser. + browser.focus(); + + // Focus the input in the options page. + await SpecialPowers.spawn(browser, [], () => { + content.document.querySelector("input").focus(); + }); + + info("input in options page should be focused, typing..."); + // Type '/'. + EventUtils.synthesizeKey("/"); + + let inputValue = await extension.awaitMessage("input-changed"); + is(inputValue, "/", "Expected input to contain a slash"); + + await closeView(win); + await extension.unload(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_options_ui_in_tab.js b/toolkit/mozapps/extensions/test/browser/browser_html_options_ui_in_tab.js new file mode 100644 index 0000000000..68faecfec0 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_options_ui_in_tab.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint max-len: ["error", 80] */ + +"use strict"; + +add_task(async function enableHtmlViews() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.htmlaboutaddons.inline-options.enabled", true]], + }); +}); + +async function testOptionsInTab({ id, options_ui_options }) { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Prefs extension", + browser_specific_settings: { gecko: { id } }, + options_ui: { + page: "options.html", + ...options_ui_options, + }, + }, + background() { + browser.test.sendMessage( + "options-url", + browser.runtime.getURL("options.html") + ); + }, + files: { + "options.html": `<script src="options.js"></script>`, + "options.js": () => { + browser.test.sendMessage("options-loaded"); + }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + let optionsUrl = await extension.awaitMessage("options-url"); + + let win = await loadInitialView("extension"); + let doc = win.document; + let aboutAddonsTab = gBrowser.selectedTab; + + let card = doc.querySelector(`addon-card[addon-id="${id}"]`); + + let prefsBtn = card.querySelector('panel-item[action="preferences"]'); + ok(!prefsBtn.hidden, "The button is not hidden"); + + info("Open the preferences page from list"); + let tabLoaded = BrowserTestUtils.waitForNewTab(gBrowser, optionsUrl); + prefsBtn.click(); + await extension.awaitMessage("options-loaded"); + BrowserTestUtils.removeTab(await tabLoaded); + + info("Load details page"); + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + // Find the expanded card. + card = doc.querySelector(`addon-card[addon-id="${id}"]`); + + info("Check that the button is still visible"); + prefsBtn = card.querySelector('panel-item[action="preferences"]'); + ok(!prefsBtn.hidden, "The button is not hidden"); + + info("Open the preferences page from details"); + tabLoaded = BrowserTestUtils.waitForNewTab(gBrowser, optionsUrl); + prefsBtn.click(); + let prefsTab = await tabLoaded; + await extension.awaitMessage("options-loaded"); + + info("Switch back to about:addons and open prefs again"); + await BrowserTestUtils.switchTab(gBrowser, aboutAddonsTab); + let tabSwitched = BrowserTestUtils.waitForEvent(gBrowser, "TabSwitchDone"); + prefsBtn.click(); + await tabSwitched; + is(gBrowser.selectedTab, prefsTab, "The prefs tab was selected"); + + BrowserTestUtils.removeTab(prefsTab); + + await closeView(win); + await extension.unload(); +} + +add_task(async function testPreferencesLink() { + let id = "prefs@mochi.test"; + await testOptionsInTab({ id, options_ui_options: { open_in_tab: true } }); +}); + +add_task(async function testPreferencesInlineDisabled() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.htmlaboutaddons.inline-options.enabled", false]], + }); + + let id = "inline-disabled@mochi.test"; + await testOptionsInTab({ id, options_ui_options: {} }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function testNoPreferences() { + let id = "no-prefs@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "No Prefs extension", + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + let doc = win.document; + + let card = doc.querySelector(`addon-card[addon-id="${id}"]`); + + info("Check button on list"); + let prefsBtn = card.querySelector('panel-item[action="preferences"]'); + ok(prefsBtn.hidden, "The button is hidden"); + + info("Load details page"); + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + // Find the expanded card. + card = doc.querySelector(`addon-card[addon-id="${id}"]`); + + info("Check that the button is still hidden on detail"); + prefsBtn = card.querySelector('panel-item[action="preferences"]'); + ok(prefsBtn.hidden, "The button is hidden"); + + await closeView(win); + await extension.unload(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_pending_updates.js b/toolkit/mozapps/extensions/test/browser/browser_html_pending_updates.js new file mode 100644 index 0000000000..f3616cd080 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_pending_updates.js @@ -0,0 +1,311 @@ +/* eslint max-len: ["error", 80] */ + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +const server = AddonTestUtils.createHttpServer(); + +const LOCALE_ADDON_ID = "postponed-langpack@mochi.test"; + +let gProvider; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.checkUpdateSecurity", false]], + }); + + // Also include a langpack with a pending postponed install. + const fakeLocalePostponedInstall = { + name: "updated langpack", + version: "2.0", + state: AddonManager.STATE_POSTPONED, + }; + + gProvider = new MockProvider(); + gProvider.createAddons([ + { + id: LOCALE_ADDON_ID, + name: "Postponed Langpack", + type: "locale", + version: "1.0", + // Mock pending upgrade property on the mocked langpack add-on. + pendingUpgrade: { + install: fakeLocalePostponedInstall, + }, + }, + ]); + + fakeLocalePostponedInstall.existingAddon = gProvider.addons[0]; + gProvider.createInstalls([fakeLocalePostponedInstall]); + + registerCleanupFunction(() => { + cleanupPendingNotifications(); + }); +}); + +function createTestExtension({ + id = "test-pending-update@test", + newManifest = {}, +}) { + function background() { + browser.runtime.onUpdateAvailable.addListener(() => { + browser.test.sendMessage("update-available"); + }); + + browser.test.sendMessage("bgpage-ready"); + } + + const serverHost = `http://localhost:${server.identity.primaryPort}`; + const updatesPath = `/ext-updates-${id}.json`; + const update_url = `${serverHost}${updatesPath}`; + + const manifest = { + name: "Test Pending Update", + browser_specific_settings: { + gecko: { id, update_url }, + }, + version: "1", + }; + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest, + // Use permanent so the add-on can be updated. + useAddonManager: "permanent", + }); + + let updateXpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + ...manifest, + ...newManifest, + version: "2", + }, + }); + + let xpiFilename = `/update-${id}.xpi`; + server.registerFile(xpiFilename, updateXpi); + AddonTestUtils.registerJSON(server, updatesPath, { + addons: { + [id]: { + updates: [ + { + version: "2", + update_link: serverHost + xpiFilename, + }, + ], + }, + }, + }); + + return { extension, updateXpi }; +} + +async function promiseUpdateAvailable(extension) { + info("Wait for the extension to receive onUpdateAvailable event"); + await extension.awaitMessage("update-available"); +} + +function expectUpdatesAvailableBadgeCount({ win, expectedNumber }) { + const categoriesSidebar = win.document.querySelector("categories-box"); + ok(categoriesSidebar, "Found the categories-box element"); + const availableButton = + categoriesSidebar.getButtonByName("available-updates"); + is( + availableButton.badgeCount, + 1, + `Expect only ${expectedNumber} available updates` + ); + ok( + !availableButton.hidden, + "Expecte the available updates category to be visible" + ); +} + +async function expectAddonInstallStatePostponed(id) { + const [addonInstall] = (await AddonManager.getAllInstalls()).filter( + install => install.existingAddon && install.existingAddon.id == id + ); + is( + addonInstall && addonInstall.state, + AddonManager.STATE_POSTPONED, + "AddonInstall is in the postponed state" + ); +} + +function expectCardOptionsButtonBadged({ id, win, hasBadge = true }) { + const card = getAddonCard(win, id); + const moreOptionsEl = card.querySelector(".more-options-button"); + is( + moreOptionsEl.classList.contains("more-options-button-badged"), + hasBadge, + `The options button should${hasBadge || "n't"} have the update badge` + ); +} + +function getCardPostponedBar({ id, win }) { + const card = getAddonCard(win, id); + return card.querySelector(".update-postponed-bar"); +} + +function waitCardAndAddonUpdated({ id, win }) { + const card = getAddonCard(win, id); + const updatedExtStarted = AddonTestUtils.promiseWebExtensionStartup(id); + const updatedCard = BrowserTestUtils.waitForEvent(card, "update"); + return Promise.all([updatedExtStarted, updatedCard]); +} + +async function testPostponedBarVisibility({ id, win, hidden = false }) { + const postponedBar = getCardPostponedBar({ id, win }); + is( + postponedBar.hidden, + hidden, + `${id} update postponed message bar should be ${ + hidden ? "hidden" : "visible" + }` + ); + + if (!hidden) { + await expectAddonInstallStatePostponed(id); + } +} + +async function assertPostponedBarVisibleInAllViews({ id, win }) { + info("Test postponed bar visibility in extension list view"); + await testPostponedBarVisibility({ id, win }); + + info("Test postponed bar visibility in available view"); + await switchView(win, "available-updates"); + await testPostponedBarVisibility({ id, win }); + + info("Test that available updates count do not include postponed langpacks"); + expectUpdatesAvailableBadgeCount({ win, expectedNumber: 1 }); + + info("Test postponed langpacks are not listed in the available updates view"); + ok( + !getAddonCard(win, LOCALE_ADDON_ID), + "Locale addon is expected to not be listed in the updates view" + ); + + info("Test that postponed bar isn't visible on postponed langpacks"); + await switchView(win, "locale"); + await testPostponedBarVisibility({ id: LOCALE_ADDON_ID, win, hidden: true }); + + info("Test postponed bar visibility in extension detail view"); + await switchView(win, "extension"); + await switchToDetailView({ win, id }); + await testPostponedBarVisibility({ id, win }); +} + +async function completePostponedUpdate({ id, win }) { + expectCardOptionsButtonBadged({ id, win, hasBadge: false }); + + await testPostponedBarVisibility({ id, win }); + + let addon = await AddonManager.getAddonByID(id); + is(addon.version, "1", "Addon version is 1"); + + const promiseUpdated = waitCardAndAddonUpdated({ id, win }); + const postponedBar = getCardPostponedBar({ id, win }); + postponedBar.querySelector("button").click(); + await promiseUpdated; + + addon = await AddonManager.getAddonByID(id); + is(addon.version, "2", "Addon version is 2"); + + await testPostponedBarVisibility({ id, win, hidden: true }); +} + +add_task(async function test_pending_update_with_prompted_permission() { + const id = "test-pending-update-with-prompted-permission@mochi.test"; + + const { extension } = createTestExtension({ + id, + newManifest: { permissions: ["<all_urls>"] }, + }); + + await extension.startup(); + await extension.awaitMessage("bgpage-ready"); + + const win = await loadInitialView("extension"); + + // Force about:addons to check for updates. + let promisePermissionHandled = handlePermissionPrompt({ + addonId: extension.id, + assertIcon: false, + }); + win.checkForUpdates(); + await promisePermissionHandled; + + await promiseUpdateAvailable(extension); + await expectAddonInstallStatePostponed(id); + + await completePostponedUpdate({ id, win }); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function test_pending_manual_install_over_existing() { + const id = "test-pending-manual-install-over-existing@mochi.test"; + + const { extension, updateXpi } = createTestExtension({ + id, + }); + + await extension.startup(); + await extension.awaitMessage("bgpage-ready"); + + let win = await loadInitialView("extension"); + + info("Manually install new xpi over the existing extension"); + const promiseInstalled = AddonTestUtils.promiseInstallFile(updateXpi); + await promiseUpdateAvailable(extension); + + await assertPostponedBarVisibleInAllViews({ id, win }); + + info("Test postponed bar visibility after reopening about:addons"); + await closeView(win); + win = await loadInitialView("extension"); + await assertPostponedBarVisibleInAllViews({ id, win }); + + await completePostponedUpdate({ id, win }); + await promiseInstalled; + + await closeView(win); + await extension.unload(); +}); + +add_task(async function test_pending_update_no_prompted_permission() { + const id = "test-pending-update-no-prompted-permission@mochi.test"; + + const { extension } = createTestExtension({ id }); + + await extension.startup(); + await extension.awaitMessage("bgpage-ready"); + + let win = await loadInitialView("extension"); + + info("Force about:addons to check for updates"); + win.checkForUpdates(); + await promiseUpdateAvailable(extension); + + await assertPostponedBarVisibleInAllViews({ id, win }); + + info("Test postponed bar visibility after reopening about:addons"); + await closeView(win); + win = await loadInitialView("extension"); + await assertPostponedBarVisibleInAllViews({ id, win }); + + await completePostponedUpdate({ id, win }); + + info("Reopen about:addons again and verify postponed bar hidden"); + await closeView(win); + win = await loadInitialView("extension"); + await testPostponedBarVisibility({ id, win, hidden: true }); + + await closeView(win); + await extension.unload(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_recent_updates.js b/toolkit/mozapps/extensions/test/browser/browser_html_recent_updates.js new file mode 100644 index 0000000000..66ce20b989 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_recent_updates.js @@ -0,0 +1,180 @@ +/* eslint max-len: ["error", 80] */ +let gProvider; + +function dateHoursAgo(hours) { + let date = new Date(); + date.setTime(date.getTime() - hours * 3600000); + return date; +} + +add_task(async function enableHtmlViews() { + gProvider = new MockProvider(); + gProvider.createAddons([ + { + id: "addon-today-2@mochi.test", + name: "Updated today two", + creator: { name: "The creator" }, + version: "3.3", + type: "extension", + updateDate: dateHoursAgo(6), + }, + { + id: "addon-today-3@mochi.test", + name: "Updated today three", + creator: { name: "The creator" }, + version: "3.3", + type: "extension", + updateDate: dateHoursAgo(9), + }, + { + id: "addon-today-1@mochi.test", + name: "Updated today", + creator: { name: "The creator" }, + version: "3.1", + type: "extension", + releaseNotesURI: "http://example.com/notes.txt", + updateDate: dateHoursAgo(1), + }, + { + id: "addon-yesterday-1@mochi.test", + name: "Updated yesterday one", + creator: { name: "The creator" }, + version: "3.3", + type: "extension", + updateDate: dateHoursAgo(15), + }, + { + id: "addon-earlier@mochi.test", + name: "Updated earlier", + creator: { name: "The creator" }, + version: "3.3", + type: "extension", + updateDate: dateHoursAgo(49), + }, + { + id: "addon-yesterday-2@mochi.test", + name: "Updated yesterday", + creator: { name: "The creator" }, + version: "3.3", + type: "extension", + updateDate: dateHoursAgo(24), + }, + { + id: "addon-lastweek@mochi.test", + name: "Updated last week", + creator: { name: "The creator" }, + version: "3.3", + type: "extension", + updateDate: dateHoursAgo(192), + }, + ]); +}); + +add_task(async function testRecentUpdatesList() { + // Load extension view first so we can mock the startOfDay property. + let win = await loadInitialView("extension"); + let doc = win.document; + let categoryUtils = new CategoryUtilities(win); + const RECENT_URL = "addons://updates/recent"; + let recentCat = categoryUtils.get("recent-updates"); + + ok(recentCat.hidden, "Recent updates category is initially hidden"); + + // Load the recent updates view. + let loaded = waitForViewLoad(win); + doc.querySelector('#page-options [action="view-recent-updates"]').click(); + await loaded; + + is( + categoryUtils.getSelectedViewId(), + RECENT_URL, + "Recent updates is selected" + ); + ok(!recentCat.hidden, "Recent updates category is now shown"); + + // Find all the add-on ids. + let list = doc.querySelector("addon-list"); + let addonsInOrder = () => + Array.from(list.querySelectorAll("addon-card")) + .map(card => card.addon.id) + .filter(id => id.endsWith("@mochi.test")); + + // Verify that the add-ons are in the right order. + Assert.deepEqual( + addonsInOrder(), + [ + "addon-today-1@mochi.test", + "addon-today-2@mochi.test", + "addon-today-3@mochi.test", + "addon-yesterday-1@mochi.test", + "addon-yesterday-2@mochi.test", + ], + "The add-ons are in the right order" + ); + + info("Check that release notes are shown on the details page"); + let card = list.querySelector( + 'addon-card[addon-id="addon-today-1@mochi.test"]' + ); + loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + card = doc.querySelector("addon-card"); + ok(card.expanded, "The card is expanded"); + ok(!card.details.tabGroup.hidden, "The tabs are shown"); + ok( + !card.details.tabGroup.querySelector('[name="release-notes"]').hidden, + "The release notes button is shown" + ); + + info("Go back to the recent updates view"); + loaded = waitForViewLoad(win); + doc.querySelector('#page-options [action="view-recent-updates"]').click(); + await loaded; + + // Find the list again. + list = doc.querySelector("addon-list"); + + info("Install a new add-on, it should be first in the list"); + // Install a new extension. + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "New extension", + browser_specific_settings: { gecko: { id: "new@mochi.test" } }, + }, + useAddonManager: "temporary", + }); + let added = BrowserTestUtils.waitForEvent(list, "add"); + await extension.startup(); + await added; + + // The new extension should now be at the top of the list. + Assert.deepEqual( + addonsInOrder(), + [ + "new@mochi.test", + "addon-today-1@mochi.test", + "addon-today-2@mochi.test", + "addon-today-3@mochi.test", + "addon-yesterday-1@mochi.test", + "addon-yesterday-2@mochi.test", + ], + "The new add-on went to the top" + ); + + // Open the detail view for the new add-on. + card = list.querySelector('addon-card[addon-id="new@mochi.test"]'); + loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + is( + categoryUtils.getSelectedViewId(), + "addons://list/extension", + "The extensions category is selected" + ); + + await closeView(win); + await extension.unload(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_recommendations.js b/toolkit/mozapps/extensions/test/browser/browser_html_recommendations.js new file mode 100644 index 0000000000..045e58d706 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_recommendations.js @@ -0,0 +1,165 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint max-len: ["error", 80] */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +const SUPPORT_URL = "http://support.allizom.org/support-dummy/"; +const SUMO_URL = SUPPORT_URL + "add-on-badges"; +const SUPPORTED_BADGES = ["recommended", "line", "verified"]; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["app.support.baseURL", SUPPORT_URL]], + }); +}); + +const server = AddonTestUtils.createHttpServer({ + hosts: ["support.allizom.org"], +}); +server.registerPathHandler("/support-dummy", (request, response) => { + response.write("Dummy"); +}); + +async function checkRecommendedBadge(id, badges = []) { + async function checkBadge() { + let card = win.document.querySelector(`addon-card[addon-id="${id}"]`); + for (let badgeName of SUPPORTED_BADGES) { + let badge = card.querySelector(`.addon-badge-${badgeName}`); + let hidden = !badges.includes(badgeName); + is( + badge.hidden, + hidden, + `badge ${badgeName} is ${hidden ? "hidden" : "shown"}` + ); + // Verify the utm params. + ok( + badge.href.startsWith(SUMO_URL), + "links to sumo correctly " + badge.href + ); + if (!hidden) { + info(`Verify the ${badgeName} badge links to the support page`); + let tabLoaded = BrowserTestUtils.waitForNewTab(gBrowser, badge.href); + EventUtils.synthesizeMouseAtCenter(badge, {}, win); + BrowserTestUtils.removeTab(await tabLoaded); + } + let url = new URL(badge.href); + is( + url.searchParams.get("utm_content"), + "promoted-addon-badge", + "content param correct" + ); + is( + url.searchParams.get("utm_source"), + "firefox-browser", + "source param correct" + ); + is( + url.searchParams.get("utm_medium"), + "firefox-browser", + "medium param correct" + ); + } + for (let badgeName of badges) { + if (!SUPPORTED_BADGES.includes(badgeName)) { + ok( + !card.querySelector(`.addon-badge-${badgeName}`), + `no badge element for ${badgeName}` + ); + } + } + return card; + } + + let win = await loadInitialView("extension"); + + // Check list view. + let card = await checkBadge(); + + // Load detail view. + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + // Check detail view. + await checkBadge(); + + await closeView(win); +} + +add_task(async function testNotRecommended() { + let id = "not-recommended@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { browser_specific_settings: { gecko: { id } } }, + useAddonManager: "temporary", + }); + await extension.startup(); + + await checkRecommendedBadge(id); + + await extension.unload(); +}); + +async function test_badged_addon(addon) { + let provider = new MockProvider(); + provider.createAddons([addon]); + await checkRecommendedBadge(addon.id, addon.recommendationStates); + + provider.unregister(); +} + +add_task(async function testRecommended() { + await test_badged_addon({ + id: "recommended@mochi.test", + isRecommended: true, + recommendationStates: ["recommended"], + name: "Recommended", + type: "extension", + }); +}); + +add_task(async function testLine() { + await test_badged_addon({ + id: "line@mochi.test", + isRecommended: false, + recommendationStates: ["line"], + name: "Line", + type: "extension", + }); +}); + +add_task(async function testVerified() { + await test_badged_addon({ + id: "verified@mochi.test", + isRecommended: false, + recommendationStates: ["verified"], + name: "Verified", + type: "extension", + }); +}); + +add_task(async function testOther() { + await test_badged_addon({ + id: "other@mochi.test", + isRecommended: false, + recommendationStates: ["other"], + name: "No Badge", + type: "extension", + }); +}); + +add_task(async function testMultiple() { + await test_badged_addon({ + id: "multiple@mochi.test", + isRecommended: false, + recommendationStates: ["verified", "recommended", "other"], + name: "Multiple", + type: "extension", + }); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_scroll_restoration.js b/toolkit/mozapps/extensions/test/browser/browser_html_scroll_restoration.js new file mode 100644 index 0000000000..e4d88bc19a --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_scroll_restoration.js @@ -0,0 +1,229 @@ +/* eslint max-len: ["error", 80] */ +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); +const server = AddonTestUtils.createHttpServer(); +const TEST_API_URL = `http://localhost:${server.identity.primaryPort}/discoapi`; + +const EXT_ID_EXTENSION = "extension@example.com"; +const EXT_ID_THEME = "theme@example.com"; + +let requestCount = 0; +server.registerPathHandler("/discoapi", (request, response) => { + // This test is expected to load the results only once, and then cache the + // results. + is(++requestCount, 1, "Expect only one discoapi request"); + + let results = { + results: [ + { + addon: { + authors: [{ name: "Some author" }], + current_version: { + files: [{ platform: "all", url: "data:," }], + }, + url: "data:,", + guid: "recommendation@example.com", + type: "extension", + }, + }, + ], + }; + response.write(JSON.stringify(results)); +}); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.getAddons.discovery.api_url", TEST_API_URL]], + }); + + let mockProvider = new MockProvider(); + mockProvider.createAddons([ + { + id: EXT_ID_EXTENSION, + name: "Mock 1", + type: "extension", + userPermissions: { + origins: ["<all_urls>"], + permissions: ["tabs"], + }, + }, + { + id: EXT_ID_THEME, + name: "Mock 2", + type: "theme", + }, + ]); +}); + +async function switchToView(win, type, param = "") { + let loaded = waitForViewLoad(win); + win.gViewController.loadView(`addons://${type}/${param}`); + await loaded; + await waitForStableLayout(win); +} + +// delta = -1 = go back. +// delta = +1 = go forwards. +async function historyGo(win, delta, expectedViewType) { + let loaded = waitForViewLoad(win); + win.history.go(delta); + await loaded; + is( + win.gViewController.currentViewId, + expectedViewType, + "Expected view after history navigation" + ); + await waitForStableLayout(win); +} + +async function waitForStableLayout(win) { + // In the test, it is important that the layout is fully stable before we + // consider the view loaded, because those affect the offset calculations. + await TestUtils.waitForCondition( + () => isLayoutStable(win), + "Waiting for layout to stabilize" + ); +} + +function isLayoutStable(win) { + // <message-bar> elements may affect the layout of a page, and therefore we + // should check whether its embedded style sheet has finished loading. + for (let bar of win.document.querySelectorAll("message-bar")) { + // Check for the existence of a CSS property from message-bar.css. + if (!win.getComputedStyle(bar).getPropertyValue("--message-bar-icon-url")) { + return false; + } + } + return true; +} + +function getScrollOffset(win) { + let { scrollTop: top, scrollLeft: left } = win.document.documentElement; + return { top, left }; +} + +// Scroll an element into view. The purpose of this is to simulate a real-world +// scenario where the user has moved part of the UI is in the viewport. +function scrollTopLeftIntoView(elem) { + elem.scrollIntoView({ block: "start", inline: "start" }); + // Sanity check: In this test, a large padding has been added to the top and + // left of the document. So when an element has been scrolled into view, the + // top and left offsets must be non-zero. + assertNonZeroScrollOffsets(getScrollOffset(elem.ownerGlobal)); +} + +function assertNonZeroScrollOffsets(offsets) { + ok(offsets.left, "Should have scrolled to the right"); + ok(offsets.top, "Should have scrolled down"); +} + +function checkScrollOffset(win, expected, msg = "") { + let actual = getScrollOffset(win); + let fuzz = AppConstants.platform == "macosx" ? 3 : 1; + isfuzzy(actual.top, expected.top, fuzz, `Top scroll offset - ${msg}`); + isfuzzy(actual.left, expected.left, fuzz, `Left scroll offset - ${msg}`); +} + +add_task(async function test_scroll_restoration() { + let win = await loadInitialView("discover"); + + // Wait until the recommendations have been loaded. These are cached after + // the first load, so we only need to wait once, at the start of the test. + await win.document.querySelector("recommended-addon-list").cardsReady; + + // Force scrollbar to appear, by adding enough space around the content. + win.document.body.style.paddingBlock = "100vh"; + win.document.body.style.paddingInline = "100vw"; + win.document.body.style.width = "300vw"; + + checkScrollOffset(win, { top: 0, left: 0 }, "initial page load"); + + scrollTopLeftIntoView(win.document.querySelector("recommended-addon-card")); + let discoOffsets = getScrollOffset(win); + assertNonZeroScrollOffsets(discoOffsets); + + // Switch from disco pane to extension list + + await switchToView(win, "list", "extension"); + checkScrollOffset(win, { top: 0, left: 0 }, "initial extension list"); + + scrollTopLeftIntoView(getAddonCard(win, EXT_ID_EXTENSION)); + let extListOffsets = getScrollOffset(win); + assertNonZeroScrollOffsets(extListOffsets); + + // Switch from extension list to details view. + + let loaded = waitForViewLoad(win); + const addonCard = getAddonCard(win, EXT_ID_EXTENSION); + // Ensure that we send a click on the control that is accessible (while a + // mouse user could also activate a card by clicking on the entire container): + const addonCardLink = addonCard.querySelector(".addon-name-link"); + addonCardLink.click(); + await loaded; + + checkScrollOffset(win, { top: 0, left: 0 }, "initial details view"); + scrollTopLeftIntoView(getAddonCard(win, EXT_ID_EXTENSION)); + let detailsOffsets = getScrollOffset(win); + assertNonZeroScrollOffsets(detailsOffsets); + + // Switch from details view back to extension list. + + await historyGo(win, -1, "addons://list/extension"); + checkScrollOffset(win, extListOffsets, "back to extension list"); + + // Now scroll to the bottom-right corner, so we can check whether the scroll + // offset is correctly restored when the extension view is loaded, even when + // the recommendations are loaded after the initial render. + ok( + win.document.querySelector("recommended-addon-card"), + "Recommendations have already been loaded" + ); + win.document.body.scrollIntoView({ block: "end", inline: "end" }); + extListOffsets = getScrollOffset(win); + assertNonZeroScrollOffsets(extListOffsets); + + // Switch back from the extension list to the details view. + + await historyGo(win, +1, `addons://detail/${EXT_ID_EXTENSION}`); + checkScrollOffset(win, detailsOffsets, "details view with default tab"); + + // Switch from the default details tab to the permissions tab. + // (this does not change the history). + win.document.querySelector(".tab-button[name='permissions']").click(); + + // Switch back from the details view to the extension list. + + await historyGo(win, -1, "addons://list/extension"); + checkScrollOffset(win, extListOffsets, "bottom-right of extension list"); + ok( + win.document.querySelector("recommended-addon-card"), + "Recommendations should have been loaded again" + ); + + // Switch back from extension list to the details view. + + await historyGo(win, +1, `addons://detail/${EXT_ID_EXTENSION}`); + // Scroll offsets are not remembered for the details view, because at the + // time of leaving the details view, the non-default tab was selected. + checkScrollOffset(win, { top: 0, left: 0 }, "details view, non-default tab"); + + // Switch back from the details view to the disco pane. + + await historyGo(win, -2, "addons://discover/"); + checkScrollOffset(win, discoOffsets, "after switching back to disco pane"); + + // Switch from disco pane to theme list. + + // Verifies that the extension list and theme lists are independent. + await switchToView(win, "list", "theme"); + checkScrollOffset(win, { top: 0, left: 0 }, "initial theme list"); + + let tabClosed = BrowserTestUtils.waitForTabClosing(gBrowser.selectedTab); + await closeView(win); + await tabClosed; +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_sitepermission_addons.js b/toolkit/mozapps/extensions/test/browser/browser_html_sitepermission_addons.js new file mode 100644 index 0000000000..3c8ce5f447 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_sitepermission_addons.js @@ -0,0 +1,178 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { SITEPERMS_ADDON_PROVIDER_PREF, SITEPERMS_ADDON_TYPE } = + ChromeUtils.importESModule( + "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs" + ); + +const html = `<!DOCTYPE html><h1>Test midi permission with synthetic site permission addon</h1>`; +const EXAMPLE_COM_URL = `https://example.com/document-builder.sjs?html=${html}`; +const EXAMPLE_ORG_URL = `https://example.org/document-builder.sjs?html=${html}`; + +async function uninstallAllSitePermissionAddons() { + const sitepermAddons = await AddonManager.getAddonsByTypes([ + SITEPERMS_ADDON_TYPE, + ]); + for (const addon of sitepermAddons) { + await addon.uninstall(); + } +} + +add_setup(async () => { + registerCleanupFunction(uninstallAllSitePermissionAddons); +}); + +add_task(async function testAboutAddonUninstall() { + if (!AddonManager.hasProvider("SitePermsAddonProvider")) { + ok( + !Services.prefs.getBoolPref(SITEPERMS_ADDON_PROVIDER_PREF), + "Expect SitePermsAddonProvider to be disabled by prefs" + ); + ok(true, "Skip test on SitePermsAddonProvider disabled"); + return; + } + + // Grant midi permission on example.com so about:addons does have a Site Permissions section + await SpecialPowers.addPermission("midi-sysex", true, EXAMPLE_COM_URL); + + info("Open an about:addon tab so AMO event listeners are registered"); + const aboutAddonWin = await loadInitialView("sitepermission"); + // loadInitialView sets the about:addon as the active one, so we can grab it here. + const aboutAddonTab = gBrowser.selectedTab; + + const addonList = aboutAddonWin.document.querySelector("addon-list"); + let addonCards = aboutAddonWin.document.querySelectorAll("addon-card"); + is( + addonCards.length, + 1, + "There's a card displayed for the example.com addon" + ); + + info("Open an example.org tab and install the midi site permission addon"); + const exampleOrgTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + EXAMPLE_ORG_URL, + true /* waitForLoad */ + ); + + let promiseAddonCardAdded = BrowserTestUtils.waitForEvent(addonList, "add"); + + info("Install midi"); + await testInstallGatedPermission( + exampleOrgTab, + () => { + content.navigator.requestMIDIAccess(); + }, + "midi" + ); + + info("Install midi-sysex as well"); + const newAddon = await testInstallGatedPermission( + exampleOrgTab, + () => { + content.navigator.requestMIDIAccess({ sysex: true }); + }, + "midi-sysex" + ); + + const newAddonId = newAddon.id; + ok( + newAddonId, + "Got the addon id for the newly installed sitepermission add-on" + ); + + info("Switch back to about:addon"); + gBrowser.selectedTab = aboutAddonTab; + + await promiseAddonCardAdded; + + is( + aboutAddonWin.document.querySelectorAll("addon-card").length, + 2, + "A new addon card has been added as expected" + ); + + const exampleOrgAddonCard = getAddonCard(aboutAddonWin, newAddonId); + + info("Remove the example.org addon"); + const promptService = mockPromptService(); + promptService._response = 0; + + let promiseRemoved = BrowserTestUtils.waitForEvent(addonList, "remove"); + exampleOrgAddonCard.querySelector("[action=remove]").click(); + await promiseRemoved; + + is( + aboutAddonWin.document.querySelectorAll("addon-card").length, + 1, + "addon card has been removed as expected" + ); + + ok( + await SpecialPowers.testPermission( + "midi", + SpecialPowers.Services.perms.UNKNOWN_ACTION, + { url: EXAMPLE_ORG_URL } + ), + "midi permission was revoked" + ); + ok( + await SpecialPowers.testPermission( + "midi-sysex", + SpecialPowers.Services.perms.UNKNOWN_ACTION, + { url: EXAMPLE_ORG_URL } + ), + "midi-sysex permission was revoked as well" + ); + + await BrowserTestUtils.removeTab(exampleOrgTab); + await close_manager(aboutAddonWin); + await uninstallAllSitePermissionAddons(); +}); + +/** + * + * Execute a function in the tab content page and check that the expected gated permission + * is set + * + * @param {Tab} tab: The tab in which we want to install the gated permission + * @param {Function} spawnCallback: function used in `SpecialPowers.spawn` that will trigger + * the install + * @param {String} expectedPermType: The name of the permission that should be granted + * @returns {Promise<Addon>} The installed addon instance + */ +async function testInstallGatedPermission( + tab, + spawnCallback, + expectedPermType +) { + let onInstallEnded = AddonTestUtils.promiseInstallEvent("onInstallEnded"); + let onAddonInstallBlockedNotification = promisePopupNotificationShown( + "addon-install-blocked" + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], spawnCallback); + + let addonInstallPanel = await onAddonInstallBlockedNotification; + let dialogPromise = promisePopupNotificationShown("addon-webext-permissions"); + addonInstallPanel.button.click(); + let installPermsDialog = await dialogPromise; + installPermsDialog.button.click(); + + const addon = await onInstallEnded.then(install => install[0].addon); + // Close the addon-installed dialog to avoid interfering with other tests + await acceptAppMenuNotificationWhenShown("addon-installed", addon.id); + + ok( + await SpecialPowers.testPermission( + expectedPermType, + SpecialPowers.Services.perms.ALLOW_ACTION, + { url: EXAMPLE_ORG_URL } + ), + `"${expectedPermType}" permission was granted` + ); + return addon; +} diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_updates.js b/toolkit/mozapps/extensions/test/browser/browser_html_updates.js new file mode 100644 index 0000000000..78ffc5678c --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_updates.js @@ -0,0 +1,750 @@ +/* eslint max-len: ["error", 80] */ + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +const server = AddonTestUtils.createHttpServer(); + +const initialAutoUpdate = AddonManager.autoUpdateDefault; +registerCleanupFunction(() => { + AddonManager.autoUpdateDefault = initialAutoUpdate; +}); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.checkUpdateSecurity", false]], + }); + + Services.telemetry.clearEvents(); + registerCleanupFunction(() => { + cleanupPendingNotifications(); + }); +}); + +function loadDetailView(win, id) { + let doc = win.document; + let card = doc.querySelector(`addon-card[addon-id="${id}"]`); + let loaded = waitForViewLoad(win); + EventUtils.synthesizeMouseAtCenter( + card.querySelector(".addon-name-link"), + { clickCount: 1 }, + win + ); + return loaded; +} + +add_task(async function testChangeAutoUpdates() { + let id = "test@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test", + browser_specific_settings: { gecko: { id } }, + }, + // Use permanent so the add-on can be updated. + useAddonManager: "permanent", + }); + + await extension.startup(); + let addon = await AddonManager.getAddonByID(id); + + let win = await loadInitialView("extension"); + let doc = win.document; + + let getInputs = updateRow => ({ + default: updatesRow.querySelector('input[value="1"]'), + on: updatesRow.querySelector('input[value="2"]'), + off: updatesRow.querySelector('input[value="0"]'), + checkForUpdate: updatesRow.querySelector('[action="update-check"]'), + }); + + await loadDetailView(win, id); + + let card = doc.querySelector(`addon-card[addon-id="${id}"]`); + ok(card.querySelector("addon-details"), "The card now has details"); + + let updatesRow = card.querySelector(".addon-detail-row-updates"); + let inputs = getInputs(updatesRow); + is(addon.applyBackgroundUpdates, 1, "Default is set"); + ok(inputs.default.checked, "The default option is selected"); + ok(inputs.checkForUpdate.hidden, "Update check is hidden"); + + inputs.on.click(); + is(addon.applyBackgroundUpdates, "2", "Updates are now enabled"); + ok(inputs.on.checked, "The on option is selected"); + ok(inputs.checkForUpdate.hidden, "Update check is hidden"); + + inputs.off.click(); + is(addon.applyBackgroundUpdates, "0", "Updates are now disabled"); + ok(inputs.off.checked, "The off option is selected"); + ok(!inputs.checkForUpdate.hidden, "Update check is visible"); + + // Go back to the list view and check the details view again. + let loaded = waitForViewLoad(win); + doc.querySelector(".back-button").click(); + await loaded; + + // Load the detail view again. + await loadDetailView(win, id); + + card = doc.querySelector(`addon-card[addon-id="${id}"]`); + updatesRow = card.querySelector(".addon-detail-row-updates"); + inputs = getInputs(updatesRow); + + ok(inputs.off.checked, "Off is still selected"); + + // Disable global updates. + let updated = BrowserTestUtils.waitForEvent(card, "update"); + AddonManager.autoUpdateDefault = false; + await updated; + + // Updates are still the same. + is(addon.applyBackgroundUpdates, "0", "Updates are now disabled"); + ok(inputs.off.checked, "The off option is selected"); + ok(!inputs.checkForUpdate.hidden, "Update check is visible"); + + // Check default. + inputs.default.click(); + is(addon.applyBackgroundUpdates, "1", "Default is set"); + ok(inputs.default.checked, "The default option is selected"); + ok(!inputs.checkForUpdate.hidden, "Update check is visible"); + + inputs.on.click(); + is(addon.applyBackgroundUpdates, "2", "Updates are now enabled"); + ok(inputs.on.checked, "The on option is selected"); + ok(inputs.checkForUpdate.hidden, "Update check is hidden"); + + // Enable updates again. + updated = BrowserTestUtils.waitForEvent(card, "update"); + AddonManager.autoUpdateDefault = true; + await updated; + + await closeView(win); + await extension.unload(); +}); + +async function setupExtensionWithUpdate( + id, + { releaseNotes, cancelUpdate } = {} +) { + let serverHost = `http://localhost:${server.identity.primaryPort}`; + let updatesPath = `/ext-updates-${id}.json`; + + let baseManifest = { + name: "Updates", + icons: { 48: "an-icon.png" }, + browser_specific_settings: { + gecko: { + id, + update_url: serverHost + updatesPath, + }, + }, + }; + + let updateXpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + ...baseManifest, + version: "2", + // Include a permission in the updated extension, to make + // sure that we trigger the permission prompt as expected + // (and that we can accept or cancel the update by observing + // the underlying observerService notification). + permissions: ["http://*.example.com/*"], + }, + }); + + let releaseNotesExtra = {}; + if (releaseNotes) { + let notesPath = "/notes.txt"; + server.registerPathHandler(notesPath, (request, response) => { + if (releaseNotes == "ERROR") { + response.setStatusLine(null, 404, "Not Found"); + } else { + response.setStatusLine(null, 200, "OK"); + response.write(releaseNotes); + } + response.processAsync(); + response.finish(); + }); + releaseNotesExtra.update_info_url = serverHost + notesPath; + } + + let xpiFilename = `/update-${id}.xpi`; + server.registerFile(xpiFilename, updateXpi); + AddonTestUtils.registerJSON(server, updatesPath, { + addons: { + [id]: { + updates: [ + { + version: "2", + update_link: serverHost + xpiFilename, + ...releaseNotesExtra, + }, + ], + }, + }, + }); + + handlePermissionPrompt({ addonId: id, reject: cancelUpdate }); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + ...baseManifest, + version: "1", + }, + // Use permanent so the add-on can be updated. + useAddonManager: "permanent", + }); + await extension.startup(); + return extension; +} + +function disableAutoUpdates(card) { + // Check button should be hidden. + let updateCheckButton = card.querySelector('button[action="update-check"]'); + ok(updateCheckButton.hidden, "The button is initially hidden"); + + // Disable updates, update check button is now visible. + card.querySelector('input[name="autoupdate"][value="0"]').click(); + ok(!updateCheckButton.hidden, "The button is now visible"); + + // There shouldn't be an update shown to the user. + assertUpdateState({ card, shown: false }); +} + +function checkForUpdate(card, expected) { + let updateCheckButton = card.querySelector('button[action="update-check"]'); + let updateFound = BrowserTestUtils.waitForEvent(card, expected); + updateCheckButton.click(); + return updateFound; +} + +function installUpdate(card, expected) { + // Install the update. + let updateInstalled = BrowserTestUtils.waitForEvent(card, expected); + let updated = BrowserTestUtils.waitForEvent(card, "update"); + card.querySelector('panel-item[action="install-update"]').click(); + return Promise.all([updateInstalled, updated]); +} + +async function findUpdatesForAddonId(id) { + let addon = await AddonManager.getAddonByID(id); + await new Promise(resolve => { + addon.findUpdates( + { onUpdateAvailable: resolve }, + AddonManager.UPDATE_WHEN_USER_REQUESTED + ); + }); +} + +function assertUpdateState({ + card, + shown, + expanded = true, + releaseNotes = false, +}) { + let menuButton = card.querySelector(".more-options-button"); + Assert.equal( + menuButton.classList.contains("more-options-button-badged"), + shown, + "The menu button is badged" + ); + let installButton = card.querySelector('panel-item[action="install-update"]'); + Assert.notEqual( + installButton.hidden, + shown, + `The install button is ${shown ? "hidden" : "shown"}` + ); + if (expanded) { + let updateCheckButton = card.querySelector('button[action="update-check"]'); + Assert.equal( + updateCheckButton.hidden, + shown, + `The update check button is ${shown ? "hidden" : "shown"}` + ); + + let { tabGroup } = card.details; + is(tabGroup.hidden, false, "The tab group is shown"); + let notesBtn = tabGroup.querySelector('[name="release-notes"]'); + is( + notesBtn.hidden, + !releaseNotes, + `The release notes button is ${releaseNotes ? "shown" : "hidden"}` + ); + } +} + +add_task(async function testUpdateAvailable() { + let id = "update@mochi.test"; + let extension = await setupExtensionWithUpdate(id); + + let win = await loadInitialView("extension"); + let doc = win.document; + + await loadDetailView(win, id); + + let card = doc.querySelector("addon-card"); + + // Disable updates and then check. + disableAutoUpdates(card); + await checkForUpdate(card, "update-found"); + + // There should now be an update. + assertUpdateState({ card, shown: true }); + + // The version was 1. + let versionRow = card.querySelector(".addon-detail-row-version"); + is(versionRow.lastChild.textContent, "1", "The version started as 1"); + + await installUpdate(card, "update-installed"); + + // The version is now 2. + versionRow = card.querySelector(".addon-detail-row-version"); + is(versionRow.lastChild.textContent, "2", "The version has updated"); + + // No update is shown again. + assertUpdateState({ card, shown: false }); + + // Check for updates again, there shouldn't be an update. + await checkForUpdate(card, "no-update"); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testReleaseNotesLoad() { + Services.telemetry.clearEvents(); + let id = "update-with-notes@mochi.test"; + let extension = await setupExtensionWithUpdate(id, { + releaseNotes: ` + <html xmlns="http://www.w3.org/1999/xhtml"> + <head><link rel="stylesheet" href="remove-me.css"/></head> + <body> + <script src="no-scripts.js"></script> + <h1>My release notes</h1> + <img src="http://example.com/tracker.png"/> + <ul> + <li onclick="alert('hi')">A thing</li> + </ul> + <a href="http://example.com/">Go somewhere</a> + </body> + </html> + `, + }); + + let win = await loadInitialView("extension"); + let doc = win.document; + + await loadDetailView(win, id); + + let card = doc.querySelector("addon-card"); + let { deck, tabGroup } = card.details; + + // Disable updates and then check. + disableAutoUpdates(card); + await checkForUpdate(card, "update-found"); + + // There should now be an update. + assertUpdateState({ card, shown: true, releaseNotes: true }); + + info("Check release notes"); + let notesBtn = tabGroup.querySelector('[name="release-notes"]'); + let notes = card.querySelector("update-release-notes"); + let loading = BrowserTestUtils.waitForEvent(notes, "release-notes-loading"); + let loaded = BrowserTestUtils.waitForEvent(notes, "release-notes-loaded"); + // Don't use notesBtn.click() since it causes an assertion to fail. + // See bug 1551621 for more info. + EventUtils.synthesizeMouseAtCenter(notesBtn, {}, win); + await loading; + is( + doc.l10n.getAttributes(notes.firstElementChild).id, + "release-notes-loading", + "The loading message is shown" + ); + await loaded; + info("Checking HTML release notes"); + let [h1, ul, a] = notes.children; + is(h1.tagName, "H1", "There's a heading"); + is(h1.textContent, "My release notes", "The heading has content"); + is(ul.tagName, "UL", "There's a list"); + is(ul.children.length, 1, "There's one item in the list"); + let [li] = ul.children; + is(li.tagName, "LI", "There's a list item"); + is(li.textContent, "A thing", "The text is set"); + ok(!li.hasAttribute("onclick"), "The onclick was removed"); + ok(!notes.querySelector("link"), "The link tag was removed"); + ok(!notes.querySelector("script"), "The script tag was removed"); + is(a.textContent, "Go somewhere", "The link text is preserved"); + is(a.href, "http://example.com/", "The link href is preserved"); + + info("Verify the link opened in a new tab"); + let tabOpened = BrowserTestUtils.waitForNewTab(gBrowser, a.href); + a.click(); + let tab = await tabOpened; + BrowserTestUtils.removeTab(tab); + + let originalContent = notes.innerHTML; + + info("Switch away and back to release notes"); + // Load details view. + let detailsBtn = tabGroup.querySelector('.tab-button[name="details"]'); + let viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed"); + detailsBtn.click(); + await viewChanged; + + // Load release notes again, verify they weren't loaded. + viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed"); + let notesCached = BrowserTestUtils.waitForEvent( + notes, + "release-notes-cached" + ); + notesBtn.click(); + await viewChanged; + await notesCached; + is(notes.innerHTML, originalContent, "The content didn't change"); + + info("Install the update to clean it up"); + await installUpdate(card, "update-installed"); + + // There's no more update but release notes are still shown. + assertUpdateState({ card, shown: false, releaseNotes: true }); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testReleaseNotesError() { + let id = "update-with-notes-error@mochi.test"; + let extension = await setupExtensionWithUpdate(id, { releaseNotes: "ERROR" }); + + let win = await loadInitialView("extension"); + let doc = win.document; + + await loadDetailView(win, id); + + let card = doc.querySelector("addon-card"); + let { deck, tabGroup } = card.details; + + // Disable updates and then check. + disableAutoUpdates(card); + await checkForUpdate(card, "update-found"); + + // There should now be an update. + assertUpdateState({ card, shown: true, releaseNotes: true }); + + info("Check release notes"); + let notesBtn = tabGroup.querySelector('[name="release-notes"]'); + let notes = card.querySelector("update-release-notes"); + let loading = BrowserTestUtils.waitForEvent(notes, "release-notes-loading"); + let errored = BrowserTestUtils.waitForEvent(notes, "release-notes-error"); + // Don't use notesBtn.click() since it causes an assertion to fail. + // See bug 1551621 for more info. + EventUtils.synthesizeMouseAtCenter(notesBtn, {}, win); + await loading; + is( + doc.l10n.getAttributes(notes.firstElementChild).id, + "release-notes-loading", + "The loading message is shown" + ); + await errored; + is( + doc.l10n.getAttributes(notes.firstElementChild).id, + "release-notes-error", + "The error message is shown" + ); + + info("Switch away and back to release notes"); + // Load details view. + let detailsBtn = tabGroup.querySelector('.tab-button[name="details"]'); + let viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed"); + detailsBtn.click(); + await viewChanged; + + // Load release notes again, verify they weren't loaded. + viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed"); + let notesCached = BrowserTestUtils.waitForEvent( + notes, + "release-notes-cached" + ); + notesBtn.click(); + await viewChanged; + await notesCached; + + info("Install the update to clean it up"); + await installUpdate(card, "update-installed"); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testUpdateCancelled() { + let id = "update@mochi.test"; + let extension = await setupExtensionWithUpdate(id, { cancelUpdate: true }); + + let win = await loadInitialView("extension"); + let doc = win.document; + + await loadDetailView(win, "update@mochi.test"); + let card = doc.querySelector("addon-card"); + + // Disable updates and then check. + disableAutoUpdates(card); + await checkForUpdate(card, "update-found"); + + // There should now be an update. + assertUpdateState({ card, shown: true }); + + // The add-on starts as version 1. + let versionRow = card.querySelector(".addon-detail-row-version"); + is(versionRow.lastChild.textContent, "1", "The version started as 1"); + + // Force the install to be cancelled. + let install = card.updateInstall; + ok(install, "There was an install found"); + + await installUpdate(card, "update-cancelled"); + + // The add-on is still version 1. + versionRow = card.querySelector(".addon-detail-row-version"); + is(versionRow.lastChild.textContent, "1", "The version hasn't changed"); + + // The update has been removed. + assertUpdateState({ card, shown: false }); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testAvailableUpdates() { + let ids = ["update1@mochi.test", "update2@mochi.test", "update3@mochi.test"]; + let addons = await Promise.all(ids.map(id => setupExtensionWithUpdate(id))); + + // Disable global add-on updates. + AddonManager.autoUpdateDefault = false; + + let win = await loadInitialView("extension"); + let doc = win.document; + let updatesMessage = doc.getElementById("updates-message"); + let categoryUtils = new CategoryUtilities(win); + + let availableCat = categoryUtils.get("available-updates"); + + ok(availableCat.hidden, "Available updates is hidden"); + is(availableCat.badgeCount, 0, "There are no updates"); + ok(updatesMessage, "There is an updates message"); + is_element_hidden(updatesMessage, "The message is hidden"); + ok(!updatesMessage.message.textContent, "The message is empty"); + ok(!updatesMessage.button.textContent, "The button is empty"); + + // Check for all updates. + let updatesFound = TestUtils.topicObserved("EM-update-check-finished"); + doc.querySelector('#page-options [action="check-for-updates"]').click(); + + is_element_visible(updatesMessage, "The message is visible"); + ok(!updatesMessage.message.textContent, "The message is empty"); + ok(updatesMessage.button.hidden, "The view updates button is hidden"); + + // Make sure the message gets populated by fluent. + await TestUtils.waitForCondition( + () => updatesMessage.message.textContent, + "wait for message text" + ); + + await updatesFound; + + // The button should be visible, and should get some text from fluent. + ok(!updatesMessage.button.hidden, "The view updates button is visible"); + await TestUtils.waitForCondition( + () => updatesMessage.button.textContent, + "wait for button text" + ); + + // Wait for the available updates count to finalize, it's async. + await BrowserTestUtils.waitForCondition(() => availableCat.badgeCount == 3); + + // The category shows the correct update count. + ok(!availableCat.hidden, "Available updates is visible"); + is(availableCat.badgeCount, 3, "There are 3 updates"); + + // Go to the available updates page. + let loaded = waitForViewLoad(win); + availableCat.click(); + await loaded; + + // Check the updates are shown. + let cards = doc.querySelectorAll("addon-card"); + is(cards.length, 3, "There are 3 cards"); + + // Each card should have an update. + for (let card of cards) { + assertUpdateState({ card, shown: true, expanded: false }); + } + + // Check the detail page for the first add-on. + await loadDetailView(win, ids[0]); + is( + categoryUtils.getSelectedViewId(), + "addons://list/extension", + "The extensions category is selected" + ); + + // Go back to the last view. + loaded = waitForViewLoad(win); + doc.querySelector(".back-button").click(); + await loaded; + + // We're back on the updates view. + is( + categoryUtils.getSelectedViewId(), + "addons://updates/available", + "The available updates category is selected" + ); + + // Find the cards again. + cards = doc.querySelectorAll("addon-card"); + is(cards.length, 3, "There are 3 cards"); + + // Install the first update. + await installUpdate(cards[0], "update-installed"); + assertUpdateState({ card: cards[0], shown: false, expanded: false }); + + // The count goes down but the card stays. + is(availableCat.badgeCount, 2, "There are only 2 updates now"); + is( + doc.querySelectorAll("addon-card").length, + 3, + "All 3 cards are still visible on the updates page" + ); + + // Install the other two updates. + await installUpdate(cards[1], "update-installed"); + assertUpdateState({ card: cards[1], shown: false, expanded: false }); + await installUpdate(cards[2], "update-installed"); + assertUpdateState({ card: cards[2], shown: false, expanded: false }); + + // The count goes down but the card stays. + is(availableCat.badgeCount, 0, "There are no more updates"); + is( + doc.querySelectorAll("addon-card").length, + 3, + "All 3 cards are still visible on the updates page" + ); + + // Enable global add-on updates again. + AddonManager.autoUpdateDefault = true; + + await closeView(win); + await Promise.all(addons.map(addon => addon.unload())); +}); + +add_task(async function testUpdatesShownOnLoad() { + let id = "has-update@mochi.test"; + let addon = await setupExtensionWithUpdate(id); + + // Find the update for our addon. + AddonManager.autoUpdateDefault = false; + await findUpdatesForAddonId(id); + + let win = await loadInitialView("extension"); + let categoryUtils = new CategoryUtilities(win); + let updatesButton = categoryUtils.get("available-updates"); + + ok(!updatesButton.hidden, "The updates button is shown"); + is(updatesButton.badgeCount, 1, "There is an update"); + + let loaded = waitForViewLoad(win); + updatesButton.click(); + await loaded; + + let cards = win.document.querySelectorAll("addon-card"); + + is(cards.length, 1, "There is one update card"); + + let card = cards[0]; + is(card.addon.id, id, "The update is for the expected add-on"); + + await installUpdate(card, "update-installed"); + + ok(!updatesButton.hidden, "The updates button is still shown"); + is(updatesButton.badgeCount, 0, "There are no more updates"); + + info("Check that the updates section is hidden when re-opened"); + await closeView(win); + win = await loadInitialView("extension"); + categoryUtils = new CategoryUtilities(win); + updatesButton = categoryUtils.get("available-updates"); + + ok(updatesButton.hidden, "Available updates is hidden"); + is(updatesButton.badgeCount, 0, "There are no updates"); + + AddonManager.autoUpdateDefault = true; + await closeView(win); + await addon.unload(); +}); + +add_task(async function testPromptOnBackgroundUpdateCheck() { + const id = "test-prompt-on-background-check@mochi.test"; + const extension = await setupExtensionWithUpdate(id); + + AddonManager.autoUpdateDefault = false; + + const addon = await AddonManager.getAddonByID(id); + await AddonTestUtils.promiseFindAddonUpdates( + addon, + AddonManager.UPDATE_WHEN_PERIODIC_UPDATE + ); + let win = await loadInitialView("extension"); + + let card = getAddonCard(win, id); + + const promisePromptInfo = promisePermissionPrompt(id); + await installUpdate(card, "update-installed"); + const promptInfo = await promisePromptInfo; + ok(promptInfo, "Got a permission prompt as expected"); + + AddonManager.autoUpdateDefault = true; + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testNoUpdateAvailableOnUnrelatedAddonCards() { + let idNoUpdate = "no-update@mochi.test"; + + let extensionNoUpdate = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + name: "TestAddonNoUpdate", + browser_specific_settings: { gecko: { id: idNoUpdate } }, + }, + }); + await extensionNoUpdate.startup(); + + let win = await loadInitialView("extension"); + + let cardNoUpdate = getAddonCard(win, idNoUpdate); + ok(cardNoUpdate, `Got AddonCard for ${idNoUpdate}`); + + // Assert that there is not an update badge + assertUpdateState({ card: cardNoUpdate, shown: false, expanded: false }); + + // Trigger a onNewInstall event by install another unrelated addon. + const XPI_URL = `${SECURE_TESTROOT}../xpinstall/amosigned.xpi`; + let install = await AddonManager.getInstallForURL(XPI_URL); + await AddonManager.installAddonFromAOM( + gBrowser.selectedBrowser, + win.document.documentURIObject, + install + ); + + // Cancel the install used to trigger the onNewInstall install event. + await install.cancel(); + // Assert that the previously installed addon isn't marked with the + // update available badge after installing an unrelated addon. + assertUpdateState({ card: cardNoUpdate, shown: false, expanded: false }); + + await closeView(win); + await extensionNoUpdate.unload(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_warning_messages.js b/toolkit/mozapps/extensions/test/browser/browser_html_warning_messages.js new file mode 100644 index 0000000000..de34cff82b --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_warning_messages.js @@ -0,0 +1,290 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint max-len: ["error", 80] */ + +"use strict"; + +let gProvider; +const { STATE_BLOCKED, STATE_SOFTBLOCKED } = Ci.nsIBlocklistService; + +const appVersion = Services.appinfo.version; +const SUPPORT_URL = Services.urlFormatter.formatURL( + Services.prefs.getStringPref("app.support.baseURL") +); + +add_setup(async function () { + gProvider = new MockProvider(); +}); + +async function checkMessageState(id, addonType, expected) { + async function checkAddonCard() { + let card = doc.querySelector(`addon-card[addon-id="${id}"]`); + let messageBar = card.querySelector(".addon-card-message"); + + if (!expected) { + ok(messageBar.hidden, "message is hidden"); + } else { + const { linkUrl, text, type } = expected; + + await BrowserTestUtils.waitForMutationCondition( + messageBar, + { attributes: true }, + () => !messageBar.hidden + ); + ok(!messageBar.hidden, "message is visible"); + + is(messageBar.getAttribute("type"), type, "message has the right type"); + Assert.deepEqual( + document.l10n.getAttributes(messageBar), + { id: `${text.id}2`, args: text.args }, + "message l10n data is set correctly" + ); + + const link = messageBar.querySelector("button"); + if (linkUrl) { + ok(!link.hidden, "link is visible"); + is( + link.getAttribute("data-l10n-id"), + `${text.id}-link`, + "link l10n id is correct" + ); + const newTab = BrowserTestUtils.waitForNewTab(gBrowser, linkUrl); + link.click(); + BrowserTestUtils.removeTab(await newTab); + } else { + ok(link.hidden, "link is hidden"); + } + } + + return card; + } + + let win = await loadInitialView(addonType); + let doc = win.document; + + // Check the list view. + ok(doc.querySelector("addon-list"), "this is a list view"); + let card = await checkAddonCard(); + + // Load the detail view. + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + // Check the detail view. + ok(!doc.querySelector("addon-list"), "this isn't a list view"); + await checkAddonCard(); + + await closeView(win); +} + +add_task(async function testNoMessageExtension() { + let id = "no-message@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { browser_specific_settings: { gecko: { id } } }, + useAddonManager: "temporary", + }); + await extension.startup(); + + await checkMessageState(id, "extension", null); + + await extension.unload(); +}); + +add_task(async function testNoMessageLangpack() { + let id = "no-message@mochi.test"; + gProvider.createAddons([ + { + appDisabled: true, + id, + name: "Signed Langpack", + signedState: AddonManager.SIGNEDSTATE_SIGNED, + type: "locale", + }, + ]); + + await checkMessageState(id, "locale", null); +}); + +add_task(async function testBlocked() { + const id = "blocked@mochi.test"; + const linkUrl = "https://example.com/addon-blocked"; + const name = "Blocked"; + gProvider.createAddons([ + { + appDisabled: true, + blocklistState: STATE_BLOCKED, + blocklistURL: linkUrl, + id, + isActive: false, + name, + }, + ]); + await checkMessageState(id, "extension", { + linkUrl, + text: { id: "details-notification-blocked", args: { name } }, + type: "error", + }); +}); + +add_task(async function testUnsignedDisabled() { + // This pref being disabled will cause the `specialpowers` addon to be + // uninstalled, which can cause a number of test failures due to features no + // longer working correctly. + // In order to avoid those issues, this code manually disables the pref, and + // ensures that `SpecialPowers` is fully re-enabled at the end of the test. + const sigPref = "xpinstall.signatures.required"; + Services.prefs.setBoolPref(sigPref, true); + + const id = "unsigned@mochi.test"; + const name = "Unsigned"; + gProvider.createAddons([ + { + appDisabled: true, + id, + name, + signedState: AddonManager.SIGNEDSTATE_MISSING, + }, + ]); + await checkMessageState(id, "extension", { + linkUrl: SUPPORT_URL + "unsigned-addons", + text: { id: "details-notification-unsigned-and-disabled", args: { name } }, + type: "error", + }); + + // Ensure that `SpecialPowers` is fully re-initialized at the end of this + // test. This requires removing the existing binding so that it's + // re-registered, re-enabling unsigned extensions, and then waiting for the + // actor to be registered and ready. + delete window.SpecialPowers; + Services.prefs.setBoolPref(sigPref, false); + await TestUtils.waitForCondition(() => { + try { + return !!windowGlobalChild.getActor("SpecialPowers"); + } catch (e) { + return false; + } + }, "wait for SpecialPowers to be reloaded"); + ok(window.SpecialPowers, "SpecialPowers should be re-defined"); +}); + +add_task(async function testUnsignedLangpackDisabled() { + const id = "unsigned-langpack@mochi.test"; + const name = "Unsigned"; + gProvider.createAddons([ + { + appDisabled: true, + id, + name, + signedState: AddonManager.SIGNEDSTATE_MISSING, + type: "locale", + }, + ]); + await checkMessageState(id, "locale", { + linkUrl: SUPPORT_URL + "unsigned-addons", + text: { id: "details-notification-unsigned-and-disabled", args: { name } }, + type: "error", + }); +}); + +add_task(async function testIncompatible() { + const id = "incompatible@mochi.test"; + const name = "Incompatible"; + gProvider.createAddons([ + { + appDisabled: true, + id, + isActive: false, + isCompatible: false, + name, + }, + ]); + await checkMessageState(id, "extension", { + text: { + id: "details-notification-incompatible", + args: { name, version: appVersion }, + }, + type: "error", + }); +}); + +add_task(async function testUnsignedEnabled() { + const id = "unsigned-allowed@mochi.test"; + const name = "Unsigned"; + gProvider.createAddons([ + { + id, + name, + signedState: AddonManager.SIGNEDSTATE_MISSING, + }, + ]); + await checkMessageState(id, "extension", { + linkUrl: SUPPORT_URL + "unsigned-addons", + text: { id: "details-notification-unsigned", args: { name } }, + type: "warning", + }); +}); + +add_task(async function testUnsignedLangpackEnabled() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.langpacks.signatures.required", false]], + }); + + const id = "unsigned-allowed-langpack@mochi.test"; + const name = "Unsigned Langpack"; + gProvider.createAddons([ + { + id, + name, + signedState: AddonManager.SIGNEDSTATE_MISSING, + type: "locale", + }, + ]); + await checkMessageState(id, "locale", { + linkUrl: SUPPORT_URL + "unsigned-addons", + text: { id: "details-notification-unsigned", args: { name } }, + type: "warning", + }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function testSoftBlocked() { + const id = "softblocked@mochi.test"; + const linkUrl = "https://example.com/addon-blocked"; + const name = "Soft Blocked"; + gProvider.createAddons([ + { + appDisabled: true, + blocklistState: STATE_SOFTBLOCKED, + blocklistURL: linkUrl, + id, + isActive: false, + name, + }, + ]); + await checkMessageState(id, "extension", { + linkUrl, + text: { id: "details-notification-softblocked", args: { name } }, + type: "warning", + }); +}); + +add_task(async function testPluginInstalling() { + const id = "plugin-installing@mochi.test"; + const name = "Plugin Installing"; + gProvider.createAddons([ + { + id, + isActive: true, + isGMPlugin: true, + isInstalled: false, + name, + type: "plugin", + }, + ]); + await checkMessageState(id, "plugin", { + text: { id: "details-notification-gmp-pending", args: { name } }, + type: "warning", + }); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_installssl.js b/toolkit/mozapps/extensions/test/browser/browser_installssl.js new file mode 100644 index 0000000000..4469b846bf --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_installssl.js @@ -0,0 +1,378 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const xpi = RELATIVE_DIR + "addons/browser_installssl.xpi"; +const redirect = RELATIVE_DIR + "redirect.sjs?"; +const SUCCESS = 0; +const NETWORK_FAILURE = AddonManager.ERROR_NETWORK_FAILURE; + +const HTTP = "http://example.com/"; +const HTTPS = "https://example.com/"; +const NOCERT = "https://nocert.example.com/"; +const SELFSIGNED = "https://self-signed.example.com/"; +const UNTRUSTED = "https://untrusted.example.com/"; +const EXPIRED = "https://expired.example.com/"; + +const PREF_INSTALL_REQUIREBUILTINCERTS = + "extensions.install.requireBuiltInCerts"; + +var gTests = []; +var gStart = 0; +var gLast = 0; +var gPendingInstall = null; + +function test() { + gStart = Date.now(); + requestLongerTimeout(4); + waitForExplicitFinish(); + + registerCleanupFunction(function () { + var cos = Cc["@mozilla.org/security/certoverride;1"].getService( + Ci.nsICertOverrideService + ); + cos.clearValidityOverride("nocert.example.com", -1, {}); + cos.clearValidityOverride("self-signed.example.com", -1, {}); + cos.clearValidityOverride("untrusted.example.com", -1, {}); + cos.clearValidityOverride("expired.example.com", -1, {}); + + if (gPendingInstall) { + gTests = []; + ok( + false, + "Timed out in the middle of downloading " + + gPendingInstall.sourceURI.spec + ); + try { + gPendingInstall.cancel(); + } catch (e) {} + } + }); + + run_next_test(); +} + +function end_test() { + info("All tests completed in " + (Date.now() - gStart) + "ms"); + finish(); +} + +function add_install_test(mainURL, redirectURL, expectedStatus) { + gTests.push([mainURL, redirectURL, expectedStatus]); +} + +function run_install_tests(callback) { + async function run_next_install_test() { + if (!gTests.length) { + callback(); + return; + } + gLast = Date.now(); + + let [mainURL, redirectURL, expectedStatus] = gTests.shift(); + if (redirectURL) { + var url = mainURL + redirect + redirectURL + xpi; + var message = + "Should have seen the right result for an install redirected from " + + mainURL + + " to " + + redirectURL; + } else { + url = mainURL + xpi; + message = + "Should have seen the right result for an install from " + mainURL; + } + + let install = await AddonManager.getInstallForURL(url); + gPendingInstall = install; + install.addListener({ + onDownloadEnded(install) { + is(SUCCESS, expectedStatus, message); + info("Install test ran in " + (Date.now() - gLast) + "ms"); + // Don't proceed with the install + install.cancel(); + gPendingInstall = null; + run_next_install_test(); + return false; + }, + + onDownloadFailed(install) { + is(install.error, expectedStatus, message); + info("Install test ran in " + (Date.now() - gLast) + "ms"); + gPendingInstall = null; + run_next_install_test(); + }, + }); + install.install(); + } + + run_next_install_test(); +} + +// Runs tests with built-in certificates required, no certificate exceptions +// and no hashes +add_test(async function test_builtin_required() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_INSTALL_REQUIREBUILTINCERTS, true]], + }); + // Tests that a simple install works as expected. + add_install_test(HTTP, null, SUCCESS); + add_install_test(HTTPS, null, NETWORK_FAILURE); + add_install_test(NOCERT, null, NETWORK_FAILURE); + add_install_test(SELFSIGNED, null, NETWORK_FAILURE); + add_install_test(UNTRUSTED, null, NETWORK_FAILURE); + add_install_test(EXPIRED, null, NETWORK_FAILURE); + + // Tests that redirecting from http to other servers works as expected + add_install_test(HTTP, HTTP, SUCCESS); + add_install_test(HTTP, HTTPS, SUCCESS); + add_install_test(HTTP, NOCERT, NETWORK_FAILURE); + add_install_test(HTTP, SELFSIGNED, NETWORK_FAILURE); + add_install_test(HTTP, UNTRUSTED, NETWORK_FAILURE); + add_install_test(HTTP, EXPIRED, NETWORK_FAILURE); + + // Tests that redirecting from valid https to other servers works as expected + add_install_test(HTTPS, HTTP, NETWORK_FAILURE); + add_install_test(HTTPS, HTTPS, NETWORK_FAILURE); + add_install_test(HTTPS, NOCERT, NETWORK_FAILURE); + add_install_test(HTTPS, SELFSIGNED, NETWORK_FAILURE); + add_install_test(HTTPS, UNTRUSTED, NETWORK_FAILURE); + add_install_test(HTTPS, EXPIRED, NETWORK_FAILURE); + + // Tests that redirecting from nocert https to other servers works as expected + add_install_test(NOCERT, HTTP, NETWORK_FAILURE); + add_install_test(NOCERT, HTTPS, NETWORK_FAILURE); + add_install_test(NOCERT, NOCERT, NETWORK_FAILURE); + add_install_test(NOCERT, SELFSIGNED, NETWORK_FAILURE); + add_install_test(NOCERT, UNTRUSTED, NETWORK_FAILURE); + add_install_test(NOCERT, EXPIRED, NETWORK_FAILURE); + + // Tests that redirecting from self-signed https to other servers works as expected + add_install_test(SELFSIGNED, HTTP, NETWORK_FAILURE); + add_install_test(SELFSIGNED, HTTPS, NETWORK_FAILURE); + add_install_test(SELFSIGNED, NOCERT, NETWORK_FAILURE); + add_install_test(SELFSIGNED, SELFSIGNED, NETWORK_FAILURE); + add_install_test(SELFSIGNED, UNTRUSTED, NETWORK_FAILURE); + add_install_test(SELFSIGNED, EXPIRED, NETWORK_FAILURE); + + // Tests that redirecting from untrusted https to other servers works as expected + add_install_test(UNTRUSTED, HTTP, NETWORK_FAILURE); + add_install_test(UNTRUSTED, HTTPS, NETWORK_FAILURE); + add_install_test(UNTRUSTED, NOCERT, NETWORK_FAILURE); + add_install_test(UNTRUSTED, SELFSIGNED, NETWORK_FAILURE); + add_install_test(UNTRUSTED, UNTRUSTED, NETWORK_FAILURE); + add_install_test(UNTRUSTED, EXPIRED, NETWORK_FAILURE); + + // Tests that redirecting from expired https to other servers works as expected + add_install_test(EXPIRED, HTTP, NETWORK_FAILURE); + add_install_test(EXPIRED, HTTPS, NETWORK_FAILURE); + add_install_test(EXPIRED, NOCERT, NETWORK_FAILURE); + add_install_test(EXPIRED, SELFSIGNED, NETWORK_FAILURE); + add_install_test(EXPIRED, UNTRUSTED, NETWORK_FAILURE); + add_install_test(EXPIRED, EXPIRED, NETWORK_FAILURE); + + run_install_tests(run_next_test); +}); + +// Runs tests without requiring built-in certificates, no certificate +// exceptions and no hashes +add_test(async function test_builtin_not_required() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_INSTALL_REQUIREBUILTINCERTS, false]], + }); + + // Tests that a simple install works as expected. + add_install_test(HTTP, null, SUCCESS); + add_install_test(HTTPS, null, SUCCESS); + add_install_test(NOCERT, null, NETWORK_FAILURE); + add_install_test(SELFSIGNED, null, NETWORK_FAILURE); + add_install_test(UNTRUSTED, null, NETWORK_FAILURE); + add_install_test(EXPIRED, null, NETWORK_FAILURE); + + // Tests that redirecting from http to other servers works as expected + add_install_test(HTTP, HTTP, SUCCESS); + add_install_test(HTTP, HTTPS, SUCCESS); + add_install_test(HTTP, NOCERT, NETWORK_FAILURE); + add_install_test(HTTP, SELFSIGNED, NETWORK_FAILURE); + add_install_test(HTTP, UNTRUSTED, NETWORK_FAILURE); + add_install_test(HTTP, EXPIRED, NETWORK_FAILURE); + + // Tests that redirecting from valid https to other servers works as expected + add_install_test(HTTPS, HTTP, NETWORK_FAILURE); + add_install_test(HTTPS, HTTPS, SUCCESS); + add_install_test(HTTPS, NOCERT, NETWORK_FAILURE); + add_install_test(HTTPS, SELFSIGNED, NETWORK_FAILURE); + add_install_test(HTTPS, UNTRUSTED, NETWORK_FAILURE); + add_install_test(HTTPS, EXPIRED, NETWORK_FAILURE); + + // Tests that redirecting from nocert https to other servers works as expected + add_install_test(NOCERT, HTTP, NETWORK_FAILURE); + add_install_test(NOCERT, HTTPS, NETWORK_FAILURE); + add_install_test(NOCERT, NOCERT, NETWORK_FAILURE); + add_install_test(NOCERT, SELFSIGNED, NETWORK_FAILURE); + add_install_test(NOCERT, UNTRUSTED, NETWORK_FAILURE); + add_install_test(NOCERT, EXPIRED, NETWORK_FAILURE); + + // Tests that redirecting from self-signed https to other servers works as expected + add_install_test(SELFSIGNED, HTTP, NETWORK_FAILURE); + add_install_test(SELFSIGNED, HTTPS, NETWORK_FAILURE); + add_install_test(SELFSIGNED, NOCERT, NETWORK_FAILURE); + add_install_test(SELFSIGNED, SELFSIGNED, NETWORK_FAILURE); + add_install_test(SELFSIGNED, UNTRUSTED, NETWORK_FAILURE); + add_install_test(SELFSIGNED, EXPIRED, NETWORK_FAILURE); + + // Tests that redirecting from untrusted https to other servers works as expected + add_install_test(UNTRUSTED, HTTP, NETWORK_FAILURE); + add_install_test(UNTRUSTED, HTTPS, NETWORK_FAILURE); + add_install_test(UNTRUSTED, NOCERT, NETWORK_FAILURE); + add_install_test(UNTRUSTED, SELFSIGNED, NETWORK_FAILURE); + add_install_test(UNTRUSTED, UNTRUSTED, NETWORK_FAILURE); + add_install_test(UNTRUSTED, EXPIRED, NETWORK_FAILURE); + + // Tests that redirecting from expired https to other servers works as expected + add_install_test(EXPIRED, HTTP, NETWORK_FAILURE); + add_install_test(EXPIRED, HTTPS, NETWORK_FAILURE); + add_install_test(EXPIRED, NOCERT, NETWORK_FAILURE); + add_install_test(EXPIRED, SELFSIGNED, NETWORK_FAILURE); + add_install_test(EXPIRED, UNTRUSTED, NETWORK_FAILURE); + add_install_test(EXPIRED, EXPIRED, NETWORK_FAILURE); + + run_install_tests(run_next_test); +}); + +// Set up overrides for the next test. +add_test(() => { + addCertOverrides().then(run_next_test); +}); + +// Runs tests with built-in certificates required, all certificate exceptions +// and no hashes +add_test(async function test_builtin_required_overrides() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_INSTALL_REQUIREBUILTINCERTS, true]], + }); + + // Tests that a simple install works as expected. + add_install_test(HTTP, null, SUCCESS); + add_install_test(HTTPS, null, NETWORK_FAILURE); + add_install_test(NOCERT, null, NETWORK_FAILURE); + add_install_test(SELFSIGNED, null, NETWORK_FAILURE); + add_install_test(UNTRUSTED, null, NETWORK_FAILURE); + add_install_test(EXPIRED, null, NETWORK_FAILURE); + + // Tests that redirecting from http to other servers works as expected + add_install_test(HTTP, HTTP, SUCCESS); + add_install_test(HTTP, HTTPS, SUCCESS); + add_install_test(HTTP, NOCERT, SUCCESS); + add_install_test(HTTP, SELFSIGNED, SUCCESS); + add_install_test(HTTP, UNTRUSTED, SUCCESS); + add_install_test(HTTP, EXPIRED, SUCCESS); + + // Tests that redirecting from valid https to other servers works as expected + add_install_test(HTTPS, HTTP, NETWORK_FAILURE); + add_install_test(HTTPS, HTTPS, NETWORK_FAILURE); + add_install_test(HTTPS, NOCERT, NETWORK_FAILURE); + add_install_test(HTTPS, SELFSIGNED, NETWORK_FAILURE); + add_install_test(HTTPS, UNTRUSTED, NETWORK_FAILURE); + add_install_test(HTTPS, EXPIRED, NETWORK_FAILURE); + + // Tests that redirecting from nocert https to other servers works as expected + add_install_test(NOCERT, HTTP, NETWORK_FAILURE); + add_install_test(NOCERT, HTTPS, NETWORK_FAILURE); + add_install_test(NOCERT, NOCERT, NETWORK_FAILURE); + add_install_test(NOCERT, SELFSIGNED, NETWORK_FAILURE); + add_install_test(NOCERT, UNTRUSTED, NETWORK_FAILURE); + add_install_test(NOCERT, EXPIRED, NETWORK_FAILURE); + + // Tests that redirecting from self-signed https to other servers works as expected + add_install_test(SELFSIGNED, HTTP, NETWORK_FAILURE); + add_install_test(SELFSIGNED, HTTPS, NETWORK_FAILURE); + add_install_test(SELFSIGNED, NOCERT, NETWORK_FAILURE); + add_install_test(SELFSIGNED, SELFSIGNED, NETWORK_FAILURE); + add_install_test(SELFSIGNED, UNTRUSTED, NETWORK_FAILURE); + add_install_test(SELFSIGNED, EXPIRED, NETWORK_FAILURE); + + // Tests that redirecting from untrusted https to other servers works as expected + add_install_test(UNTRUSTED, HTTP, NETWORK_FAILURE); + add_install_test(UNTRUSTED, HTTPS, NETWORK_FAILURE); + add_install_test(UNTRUSTED, NOCERT, NETWORK_FAILURE); + add_install_test(UNTRUSTED, SELFSIGNED, NETWORK_FAILURE); + add_install_test(UNTRUSTED, UNTRUSTED, NETWORK_FAILURE); + add_install_test(UNTRUSTED, EXPIRED, NETWORK_FAILURE); + + // Tests that redirecting from expired https to other servers works as expected + add_install_test(EXPIRED, HTTP, NETWORK_FAILURE); + add_install_test(EXPIRED, HTTPS, NETWORK_FAILURE); + add_install_test(EXPIRED, NOCERT, NETWORK_FAILURE); + add_install_test(EXPIRED, SELFSIGNED, NETWORK_FAILURE); + add_install_test(EXPIRED, UNTRUSTED, NETWORK_FAILURE); + add_install_test(EXPIRED, EXPIRED, NETWORK_FAILURE); + + run_install_tests(run_next_test); +}); + +// Runs tests without requiring built-in certificates, all certificate +// exceptions and no hashes +add_test(async function test_builtin_not_required_overrides() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_INSTALL_REQUIREBUILTINCERTS, false]], + }); + + // Tests that a simple install works as expected. + add_install_test(HTTP, null, SUCCESS); + add_install_test(HTTPS, null, SUCCESS); + add_install_test(NOCERT, null, SUCCESS); + add_install_test(SELFSIGNED, null, SUCCESS); + add_install_test(UNTRUSTED, null, SUCCESS); + add_install_test(EXPIRED, null, SUCCESS); + + // Tests that redirecting from http to other servers works as expected + add_install_test(HTTP, HTTP, SUCCESS); + add_install_test(HTTP, HTTPS, SUCCESS); + add_install_test(HTTP, NOCERT, SUCCESS); + add_install_test(HTTP, SELFSIGNED, SUCCESS); + add_install_test(HTTP, UNTRUSTED, SUCCESS); + add_install_test(HTTP, EXPIRED, SUCCESS); + + // Tests that redirecting from valid https to other servers works as expected + add_install_test(HTTPS, HTTP, NETWORK_FAILURE); + add_install_test(HTTPS, HTTPS, SUCCESS); + add_install_test(HTTPS, NOCERT, SUCCESS); + add_install_test(HTTPS, SELFSIGNED, SUCCESS); + add_install_test(HTTPS, UNTRUSTED, SUCCESS); + add_install_test(HTTPS, EXPIRED, SUCCESS); + + // Tests that redirecting from nocert https to other servers works as expected + add_install_test(NOCERT, HTTP, NETWORK_FAILURE); + add_install_test(NOCERT, HTTPS, SUCCESS); + add_install_test(NOCERT, NOCERT, SUCCESS); + add_install_test(NOCERT, SELFSIGNED, SUCCESS); + add_install_test(NOCERT, UNTRUSTED, SUCCESS); + add_install_test(NOCERT, EXPIRED, SUCCESS); + + // Tests that redirecting from self-signed https to other servers works as expected + add_install_test(SELFSIGNED, HTTP, NETWORK_FAILURE); + add_install_test(SELFSIGNED, HTTPS, SUCCESS); + add_install_test(SELFSIGNED, NOCERT, SUCCESS); + add_install_test(SELFSIGNED, SELFSIGNED, SUCCESS); + add_install_test(SELFSIGNED, UNTRUSTED, SUCCESS); + add_install_test(SELFSIGNED, EXPIRED, SUCCESS); + + // Tests that redirecting from untrusted https to other servers works as expected + add_install_test(UNTRUSTED, HTTP, NETWORK_FAILURE); + add_install_test(UNTRUSTED, HTTPS, SUCCESS); + add_install_test(UNTRUSTED, NOCERT, SUCCESS); + add_install_test(UNTRUSTED, SELFSIGNED, SUCCESS); + add_install_test(UNTRUSTED, UNTRUSTED, SUCCESS); + add_install_test(UNTRUSTED, EXPIRED, SUCCESS); + + // Tests that redirecting from expired https to other servers works as expected + add_install_test(EXPIRED, HTTP, NETWORK_FAILURE); + add_install_test(EXPIRED, HTTPS, SUCCESS); + add_install_test(EXPIRED, NOCERT, SUCCESS); + add_install_test(EXPIRED, SELFSIGNED, SUCCESS); + add_install_test(EXPIRED, UNTRUSTED, SUCCESS); + add_install_test(EXPIRED, EXPIRED, SUCCESS); + + run_install_tests(run_next_test); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_installtrigger_install.js b/toolkit/mozapps/extensions/test/browser/browser_installtrigger_install.js new file mode 100644 index 0000000000..1d50da2833 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_installtrigger_install.js @@ -0,0 +1,362 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +const XPI_URL = `${SECURE_TESTROOT}../xpinstall/amosigned.xpi`; +const XPI_ADDON_ID = "amosigned-xpi@tests.mozilla.org"; + +AddonTestUtils.initMochitest(this); + +AddonTestUtils.hookAMTelemetryEvents(); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.webapi.testing", true], + ["extensions.install.requireBuiltInCerts", false], + ["extensions.InstallTrigger.enabled", true], + ["extensions.InstallTriggerImpl.enabled", true], + // Relax the user input requirements while running this test. + ["xpinstall.userActivation.required", false], + ], + }); + + PermissionTestUtils.add( + "https://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + registerCleanupFunction(async () => { + PermissionTestUtils.remove("https://example.com", "install"); + await SpecialPowers.popPrefEnv(); + }); +}); + +async function testInstallTrigger( + msg, + tabURL, + contentFnArgs, + contentFn, + expectedTelemetryInfo, + expectBlockedOrigin +) { + // Clear collected events before each test, otherwise the test would fail + // intermittently when Glean is going to submit the events and clear them + // after reaching the max events length limit. + Services.fog.testResetFOG(); + + await BrowserTestUtils.withNewTab(tabURL, async browser => { + if (expectBlockedOrigin) { + const promiseOriginBlocked = TestUtils.topicObserved( + "addon-install-origin-blocked" + ); + await SpecialPowers.spawn(browser, contentFnArgs, contentFn); + const [subject] = await promiseOriginBlocked; + const installId = subject.wrappedJSObject.installs[0].installId; + + let gleanEvents = AddonTestUtils.getAMGleanEvents("install", { + install_id: `${installId}`, + step: "site_blocked", + }); + ok(!!gleanEvents.length, "Found Glean events for the blocked install."); + Assert.deepEqual( + { source: gleanEvents[0].source }, + expectedTelemetryInfo, + `Got expected Glean telemetry on test case "${msg}"` + ); + + // Select all telemetry events related to the installId. + const telemetryEvents = AddonTestUtils.getAMTelemetryEvents().filter( + ev => { + return ( + ev.method === "install" && + ev.value === `${installId}` && + ev.extra.step === "site_blocked" + ); + } + ); + ok( + !!telemetryEvents.length, + "Found telemetry events for the blocked install" + ); + + const source = telemetryEvents[0]?.extra.source; + Assert.deepEqual( + { source }, + expectedTelemetryInfo, + `Got expected telemetry on test case "${msg}"` + ); + return; + } + + let installPromptPromise = promisePopupNotificationShown( + "addon-webext-permissions" + ).then(panel => { + panel.button.click(); + }); + + let promptPromise = acceptAppMenuNotificationWhenShown( + "addon-installed", + XPI_ADDON_ID + ); + + await SpecialPowers.spawn(browser, contentFnArgs, contentFn); + + await Promise.all([installPromptPromise, promptPromise]); + + let addon = await promiseAddonByID(XPI_ADDON_ID); + + registerCleanupFunction(async () => { + await addon.uninstall(); + }); + + // Check that the expected installTelemetryInfo has been stored in the + // addon details. + AddonTestUtils.checkInstallInfo( + addon, + { method: "installTrigger", ...expectedTelemetryInfo }, + `on "${msg}"` + ); + + await addon.uninstall(); + }); +} + +add_task(function testInstallAfterHistoryPushState() { + return testInstallTrigger( + "InstallTrigger after history.pushState", + SECURE_TESTROOT, + [SECURE_TESTROOT, XPI_URL], + (secureTestRoot, xpiURL) => { + // `sourceURL` should match the exact location, even after a location + // update using the history API. In this case, we update the URL with + // query parameters and expect `sourceURL` to contain those parameters. + content.history.pushState( + {}, // state + "", // title + `${secureTestRoot}?some=query&par=am` + ); + content.InstallTrigger.install({ URL: xpiURL }); + }, + { + source: "test-host", + sourceURL: + "https://example.com/browser/toolkit/mozapps/extensions/test/browser/?some=query&par=am", + } + ); +}); + +add_task(async function testInstallTriggerFromSubframe() { + function runTestCase(msg, tabURL, testFrameAttrs, expected) { + info( + `InstallTrigger from iframe test: ${msg} - frame attributes ${JSON.stringify( + testFrameAttrs + )}` + ); + return testInstallTrigger( + msg, + tabURL, + [XPI_URL, testFrameAttrs], + async (xpiURL, frameAttrs) => { + const frame = content.document.createElement("iframe"); + if (frameAttrs) { + for (const attr of Object.keys(frameAttrs)) { + let value = frameAttrs[attr]; + if (value === "blob:") { + const blob = new content.Blob(["blob-testpage"]); + value = content.URL.createObjectURL(blob, "text/html"); + } + frame[attr] = value; + } + } + const promiseLoaded = new Promise(resolve => + frame.addEventListener("load", resolve, { once: true }) + ); + content.document.body.appendChild(frame); + await promiseLoaded; + frame.contentWindow.InstallTrigger.install({ URL: xpiURL }); + }, + expected.telemetryInfo, + expected.blockedOrigin + ); + } + + // On Windows "file:///" does not load the default files index html page + // and the test would get stuck. + const fileURL = AppConstants.platform === "win" ? "file:///C:/" : "file:///"; + + const expected = { + http: { + telemetryInfo: { + source: "test-host", + sourceURL: + "https://example.com/browser/toolkit/mozapps/extensions/test/browser/", + }, + blockedOrigin: false, + }, + httpBlob: { + telemetryInfo: { + source: "test-host", + // Example: "blob:https://example.com/BLOB_URL_UUID" + sourceURL: /^blob:https:\/\/example\.com\//, + }, + blockedOrigin: false, + }, + file: { + telemetryInfo: { + source: "unknown", + sourceURL: fileURL, + }, + blockedOrigin: false, + }, + fileBlob: { + telemetryInfo: { + source: "unknown", + // Example: "blob:null/BLOB_URL_UUID" + sourceURL: /^blob:null\//, + }, + blockedOrigin: false, + }, + httpBlockedOnOrigin: { + telemetryInfo: { + source: "test-host", + }, + blockedOrigin: true, + }, + otherBlockedOnOrigin: { + telemetryInfo: { + source: "unknown", + }, + blockedOrigin: true, + }, + }; + + const testCases = [ + ["blank iframe with no attributes", SECURE_TESTROOT, {}, expected.http], + + // These are blocked by a Firefox doorhanger and the user can't allow it neither. + [ + "http page iframe src='blob:...'", + SECURE_TESTROOT, + { src: "blob:" }, + expected.httpBlockedOnOrigin, + ], + [ + "file page iframe src='blob:...'", + fileURL, + { src: "blob:" }, + expected.otherBlockedOnOrigin, + ], + [ + "iframe srcdoc=''", + SECURE_TESTROOT, + { srcdoc: "" }, + expected.httpBlockedOnOrigin, + ], + [ + "blank iframe embedded into a top-level sandbox page", + `${SECURE_TESTROOT}sandboxed.html`, + {}, + expected.httpBlockedOnOrigin, + ], + [ + "blank iframe with sandbox='allow-scripts'", + SECURE_TESTROOT, + { sandbox: "allow-scripts" }, + expected.httpBlockedOnOrigin, + ], + [ + "iframe srcdoc='' sandbox='allow-scripts'", + SECURE_TESTROOT, + { srcdoc: "", sandbox: "allow-scripts" }, + expected.httpBlockedOnOrigin, + ], + [ + "http page iframe src='blob:...' sandbox='allow-scripts'", + SECURE_TESTROOT, + { src: "blob:", sandbox: "allow-scripts" }, + expected.httpBlockedOnOrigin, + ], + [ + "iframe src='data:...'", + SECURE_TESTROOT, + { src: "data:text/html,data-testpage" }, + expected.httpBlockedOnOrigin, + ], + [ + "blank frame embedded in a data url", + "data:text/html,data-testpage", + {}, + expected.otherBlockedOnOrigin, + ], + [ + "blank frame embedded into a about:blank page", + "about:blank", + {}, + expected.otherBlockedOnOrigin, + ], + ]; + + for (const testCase of testCases) { + await runTestCase(...testCase); + } +}); + +add_task(function testInstallBlankFrameNestedIntoBlobURLPage() { + return testInstallTrigger( + "Blank frame nested into a blob url page", + SECURE_TESTROOT, + [XPI_URL], + async xpiURL => { + const url = content.URL.createObjectURL( + new content.Blob(["blob-testpage"]), + "text/html" + ); + const topframe = content.document.createElement("iframe"); + topframe.src = url; + const topframeLoaded = new Promise(resolve => { + topframe.addEventListener("load", resolve, { once: true }); + }); + content.document.body.appendChild(topframe); + await topframeLoaded; + const subframe = topframe.contentDocument.createElement("iframe"); + topframe.contentDocument.body.appendChild(subframe); + subframe.contentWindow.InstallTrigger.install({ URL: xpiURL }); + }, + { + source: "test-host", + }, + /* expectBlockedOrigin */ true + ); +}); + +add_task(function testInstallTriggerTopLevelDataURL() { + return testInstallTrigger( + "Blank frame nested into a blob url page", + "data:text/html,testpage", + [XPI_URL], + async xpiURL => { + this.content.InstallTrigger.install({ URL: xpiURL }); + }, + { + source: "unknown", + }, + /* expectBlockedOrigin */ true + ); +}); + +add_task(function teardown_clearUnexamitedTelemetry() { + // Clear collected telemetry events when we are not going to run any assertion on them. + // (otherwise the test will fail because of unexamined telemetry events). + AddonTestUtils.getAMTelemetryEvents(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_local_install.js b/toolkit/mozapps/extensions/test/browser/browser_local_install.js new file mode 100644 index 0000000000..5200b69e39 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_local_install.js @@ -0,0 +1,245 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +const XPI_INCOMPATIBLE_ID = "incompatible-xpi@tests.mozilla.org"; +// NOTE: we are using an HTTP url on purpose here, the test case fails +// otherwise... We disable `AddonManager.checkUpdateSecurity` to allow +// retrieving updates from HTTP (which is restored in a +// `registerCleanupFunction()` or at the end of the task). +// +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const BASE_URL = "http://fake-updates.example.com"; + +const server = AddonTestUtils.createHttpServer({ + hosts: ["fake-updates.example.com"], +}); + +const UPDATE_ENTRY_COMPATIBLE = { + // NOTE: this version must be the exact same one associated than the + // initially incompatible XPI, otherwise it won't override the initial + // compatibility range. + // See the check in `AddonUpdateChecker.getCompatibilityUpdate` here: + // https://searchfox.org/mozilla-central/rev/4044c340/toolkit/mozapps/extensions/internal/AddonUpdateChecker.sys.mjs#489 + version: "4.0", + // An empty compatibility range will make this update to be overriding the + // incompatible range in the xpi and makes the xpi version to be considered + // compatible. + applications: { gecko: {} }, +}; + +const UPDATE_ENTRY_INCOMPATIBLE = { + ...UPDATE_ENTRY_COMPATIBLE, + // This update entry instead is including a compatibility range that would + // makes the xpi version being installed to be considered still incompatible. + applications: { + gecko: { + strict_min_version: "41", + strict_max_version: "41.*", + }, + }, +}; + +AddonTestUtils.registerJSON(server, "/updates-still-incompatible.json", { + addons: { + [XPI_INCOMPATIBLE_ID]: { + updates: [UPDATE_ENTRY_INCOMPATIBLE], + }, + }, +}); + +AddonTestUtils.registerJSON(server, "/updates-now-compatible.json", { + addons: { + [XPI_INCOMPATIBLE_ID]: { + updates: [UPDATE_ENTRY_COMPATIBLE], + }, + }, +}); + +add_task(async function test_local_install_blocklisted() { + let id = "amosigned-xpi@tests.mozilla.org"; + let version = "2.1"; + + await AddonTestUtils.loadBlocklistRawData({ + extensionsMLBF: [ + { + stash: { blocked: [`${id}:${version}`], unblocked: [] }, + stash_time: 0, + }, + ], + }); + let needsCleanupBlocklist = true; + const cleanupBlocklist = async () => { + if (!needsCleanupBlocklist) { + return; + } + await AddonTestUtils.loadBlocklistRawData({ + extensionsMLBF: [ + { + stash: { blocked: [], unblocked: [] }, + stash_time: 0, + }, + ], + }); + needsCleanupBlocklist = false; + }; + registerCleanupFunction(cleanupBlocklist); + + const xpiFilePath = getTestFilePath("../xpinstall/amosigned.xpi"); + const xpiFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + xpiFile.initWithPath(xpiFilePath); + ok(xpiFile.exists(), "Expect the xpi file to exist"); + const xpiFileURI = Services.io.newFileURI(xpiFile); + + let install = await AddonManager.getInstallForURL(xpiFileURI.spec, { + telemetryInfo: { source: "file-url" }, + }); + const promiseInstallFailed = BrowserUtils.promiseObserved( + "addon-install-failed", + subject => { + return subject.wrappedJSObject.installs[0] == install; + } + ); + + AddonManager.installAddonFromWebpage( + "application/x-xpinstall", + gBrowser.selectedBrowser, + Services.scriptSecurityManager.getSystemPrincipal(), + install + ); + + info("Wait for addon-install-failed to be notified"); + await promiseInstallFailed; + Assert.equal( + install.error, + AddonManager.ERROR_BLOCKLISTED, + "LocalInstall cancelled with the expected error" + ); + + await cleanupBlocklist(); +}); + +add_task(async function test_local_install_incompatible() { + const xpiFilePath = getTestFilePath("../xpinstall/incompatible.xpi"); + const xpiFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + xpiFile.initWithPath(xpiFilePath); + ok(xpiFile.exists(), "Expect the xpi file to exist"); + const xpiFileURI = Services.io.newFileURI(xpiFile); + + const installTestExtension = async ({ expectIncompatible }) => { + let install = await AddonManager.getInstallForURL(xpiFileURI.spec, { + telemetryInfo: { source: "file-url" }, + }); + const promiseInstallDone = expectIncompatible + ? BrowserUtils.promiseObserved( + "addon-install-failed", + subject => subject.wrappedJSObject.installs[0] == install + ) + : BrowserUtils.promiseObserved( + "webextension-permission-prompt", + subject => subject.wrappedJSObject.info.addon == install.addon + ); + + AddonManager.installAddonFromWebpage( + "application/x-xpinstall", + gBrowser.selectedBrowser, + Services.scriptSecurityManager.getSystemPrincipal(), + install + ); + + if (expectIncompatible) { + info("Wait for addon-install-failed to be notified"); + await promiseInstallDone; + Assert.equal( + install.error, + AddonManager.ERROR_INCOMPATIBLE, + "LocalInstall cancelled with the expected error" + ); + } else { + info("Wait for webextension-permission-prompt to be notified"); + await promiseInstallDone; + Assert.equal( + install.error, + 0, + "no error expected on the LocalInstall instance" + ); + Assert.equal( + install.state, + AddonManager.STATE_DOWNLOADED, + "Got the expected LocalInstall state" + ); + Assert.ok( + install.addon.isCompatible, + "updated Addon XPI is expected to be compatible" + ); + Assert.equal( + install.addon.version, + "4.0", + "Addon version expected to match the updated xpi file" + ); + // Cancel the installation, before exiting the test. + await install.cancel(); + } + }; + + info("Test incompatible xpi without a compatibility override"); + // Use a new tab to make sure the doorhanger will be gone when + // the test tab is being removed (same when repeating the + // test with expectIncompatible set to false). + await BrowserTestUtils.withNewTab("about:blank", async () => { + await installTestExtension({ expectIncompatible: true }); + }); + + // Add the prefs to ignore signature checks for this test (allowed on all + // channels while running in automation). + SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.update.url", `${BASE_URL}/updates.json`], + ["xpinstall.signatures.required", false], + ["extensions.ui.ignoreUnsigned", true], + ], + }); + AddonManager.checkUpdateSecurity = false; + registerCleanupFunction(() => { + AddonManager.checkUpdateSecurity = true; + }); + + info( + "Test incompatible xpi with a compatibility override that is still incompatible" + ); + // Add the prefs to provide a compatibility range override which is still + // incompatible. + SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.update.url", `${BASE_URL}/updates-still-incompatible.json`], + ], + }); + await BrowserTestUtils.withNewTab("about:blank", async () => { + await installTestExtension({ expectIncompatible: true }); + }); + SpecialPowers.popPrefEnv(); + + info( + "Test incompatible xpi with a compatibility override that makes it compatible" + ); + // Add the prefs to provide a compatibility range override which is + // compatible. + SpecialPowers.pushPrefEnv({ + set: [["extensions.update.url", `${BASE_URL}/updates-now-compatible.json`]], + }); + await BrowserTestUtils.withNewTab("about:blank", async () => { + await installTestExtension({ expectIncompatible: false }); + }); + SpecialPowers.popPrefEnv(); + + SpecialPowers.popPrefEnv(); + AddonManager.checkUpdateSecurity = true; +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts.js b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts.js new file mode 100644 index 0000000000..aee47dd049 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts.js @@ -0,0 +1,331 @@ +"use strict"; + +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally( + /Message manager disconnected/ +); + +function extensionShortcutsReady(id) { + let extension = WebExtensionPolicy.getByID(id).extension; + return BrowserTestUtils.waitForCondition(() => { + return extension.shortcuts.keysetsMap.has(window); + }, "Wait for add-on keyset to be registered"); +} + +async function loadShortcutsView() { + // Load the theme view initially so we can verify that the category is switched + // to "extension" when the shortcuts view is loaded. + let win = await loadInitialView("theme"); + let categoryUtils = new CategoryUtilities(win); + + is( + categoryUtils.getSelectedViewId(), + "addons://list/theme", + "The theme category is selected" + ); + + let shortcutsLink = win.document.querySelector( + '#page-options [action="manage-shortcuts"]' + ); + ok(!shortcutsLink.hidden, "The shortcuts link is visible"); + + let loaded = waitForViewLoad(win); + shortcutsLink.click(); + await loaded; + + is( + categoryUtils.getSelectedViewId(), + "addons://list/extension", + "The extension category is now selected" + ); + + return win; +} + +add_task(async function testUpdatingCommands() { + let commands = { + commandZero: {}, + commandOne: { + suggested_key: { default: "Shift+Alt+7" }, + }, + commandTwo: { + description: "Command Two!", + suggested_key: { default: "Alt+4" }, + }, + _execute_browser_action: { + suggested_key: { default: "Shift+Alt+9" }, + }, + }; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + commands, + browser_action: { default_popup: "popup.html" }, + }, + background() { + browser.commands.onCommand.addListener(commandName => { + browser.test.sendMessage("oncommand", commandName); + }); + browser.test.sendMessage("ready"); + }, + useAddonManager: "temporary", + }); + + await extension.startup(); + await extension.awaitMessage("ready"); + await extensionShortcutsReady(extension.id); + + async function checkShortcut(name, key, modifiers) { + EventUtils.synthesizeKey(key, modifiers); + let message = await extension.awaitMessage("oncommand"); + is( + message, + name, + `Expected onCommand listener to fire with the correct name: ${name}` + ); + } + + // Load the about:addons shortcut view before verify that emitting + // the key events does trigger the expected extension commands. + // There is apparently a race (more likely to be triggered on an + // optimized build) between: + // - the new opened browser window to be ready to listen for the + // keyboard events that are expected to triggered one of the key + // in the extension keyset + // - and the test calling EventUtils.syntesizeKey to test that + // the expected extension command listener is notified. + // + // Loading the shortcut view before calling checkShortcut seems to be + // enough to consistently avoid that race condition. + let win = await loadShortcutsView(); + + // Check that the original shortcuts work. + await checkShortcut("commandOne", "7", { shiftKey: true, altKey: true }); + await checkShortcut("commandTwo", "4", { altKey: true }); + + let doc = win.document; + + let card = doc.querySelector(`.card[addon-id="${extension.id}"]`); + ok(card, `There is a card for the extension`); + + let inputs = card.querySelectorAll(".shortcut-input"); + is( + inputs.length, + Object.keys(commands).length, + "There is an input for each command" + ); + + let nameOrder = Array.from(inputs).map(input => input.getAttribute("name")); + Assert.deepEqual( + nameOrder, + ["commandOne", "commandTwo", "_execute_browser_action", "commandZero"], + "commandZero should be last since it is unset" + ); + + let count = 1; + for (let input of inputs) { + // Change the shortcut. + input.focus(); + EventUtils.synthesizeKey("8", { shiftKey: true, altKey: true }); + count++; + + // Wait for the shortcut attribute to change. + await BrowserTestUtils.waitForCondition( + () => input.getAttribute("shortcut") == "Alt+Shift+8", + "Wait for shortcut to update to Alt+Shift+8" + ); + + // Check that the change worked (but skip if browserAction). + if (input.getAttribute("name") != "_execute_browser_action") { + await checkShortcut(input.getAttribute("name"), "8", { + shiftKey: true, + altKey: true, + }); + } + + // Change it again so it doesn't conflict with the next command. + input.focus(); + EventUtils.synthesizeKey(count.toString(), { + shiftKey: true, + altKey: true, + }); + await BrowserTestUtils.waitForCondition( + () => input.getAttribute("shortcut") == `Alt+Shift+${count}`, + `Wait for shortcut to update to Alt+Shift+${count}` + ); + } + + // Check that errors can be shown. + let input = inputs[0]; + let error = doc.querySelector(".error-message"); + let label = error.querySelector(".error-message-label"); + is(error.style.visibility, "hidden", "The error is initially hidden"); + + // Try a shortcut with only shift for a modifier. + input.focus(); + EventUtils.synthesizeKey("J", { shiftKey: true }); + let possibleErrors = ["shortcuts-modifier-mac", "shortcuts-modifier-other"]; + ok(possibleErrors.includes(label.dataset.l10nId), `The message is set`); + is(error.style.visibility, "visible", "The error is shown"); + + // Escape should clear the focus and hide the error. + is(doc.activeElement, input, "The input is focused"); + EventUtils.synthesizeKey("Escape", {}); + Assert.notEqual(doc.activeElement, input, "The input is no longer focused"); + is(error.style.visibility, "hidden", "The error is hidden"); + + // Check if assigning already assigned shortcut is prevented. + input.focus(); + EventUtils.synthesizeKey("2", { shiftKey: true, altKey: true }); + is(label.dataset.l10nId, "shortcuts-exists", `The message is set`); + is(error.style.visibility, "visible", "The error is shown"); + + // Check the label uses the description first, and has a default for the special cases. + function checkLabel(name, value) { + let input = doc.querySelector(`.shortcut-input[name="${name}"]`); + let label = input.previousElementSibling; + if (label.dataset.l10nId) { + is(label.dataset.l10nId, value, "The l10n-id is set"); + } else { + is(label.textContent, value, "The textContent is set"); + } + } + checkLabel("commandOne", "commandOne"); + checkLabel("commandTwo", "Command Two!"); + checkLabel("_execute_browser_action", "shortcuts-browserAction2"); + + await closeView(win); + await extension.unload(); +}); + +async function startExtensionWithCommands(numCommands) { + let commands = {}; + + for (let i = 0; i < numCommands; i++) { + commands[`command-${i}`] = {}; + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + commands, + }, + background() { + browser.test.sendMessage("ready"); + }, + useAddonManager: "temporary", + }); + + await extension.startup(); + await extension.awaitMessage("ready"); + await extensionShortcutsReady(extension.id); + + return extension; +} + +add_task(async function testExpanding() { + const numCommands = 7; + const visibleCommands = 5; + + let extension = await startExtensionWithCommands(numCommands); + + let win = await loadShortcutsView(); + let doc = win.document; + + let card = doc.querySelector(`.card[addon-id="${extension.id}"]`); + ok(!card.hasAttribute("expanded"), "The card is not expanded"); + + let shortcutRows = card.querySelectorAll(".shortcut-row"); + is(shortcutRows.length, numCommands, `There are ${numCommands} shortcuts`); + + function assertCollapsedVisibility() { + for (let i = 0; i < shortcutRows.length; i++) { + let row = shortcutRows[i]; + if (i < visibleCommands) { + Assert.notEqual( + getComputedStyle(row).display, + "none", + `The first ${visibleCommands} rows are visible` + ); + } else { + is(getComputedStyle(row).display, "none", "The other rows are hidden"); + } + } + } + + // Check the visibility of the rows. + assertCollapsedVisibility(); + + let expandButton = card.querySelector(".expand-button"); + ok(expandButton, "There is an expand button"); + let l10nAttrs = doc.l10n.getAttributes(expandButton); + is(l10nAttrs.id, "shortcuts-card-expand-button", "The expand text is shown"); + is( + l10nAttrs.args.numberToShow, + numCommands - visibleCommands, + "The number to be shown is set on the expand button" + ); + + // Expand the card. + expandButton.click(); + + is(card.getAttribute("expanded"), "true", "The card is now expanded"); + + for (let row of shortcutRows) { + Assert.notEqual( + getComputedStyle(row).display, + "none", + "All the rows are visible" + ); + } + + // The collapse text is now shown. + l10nAttrs = doc.l10n.getAttributes(expandButton); + is( + l10nAttrs.id, + "shortcuts-card-collapse-button", + "The colapse text is shown" + ); + + // Collapse the card. + expandButton.click(); + + ok(!card.hasAttribute("expanded"), "The card is now collapsed again"); + + assertCollapsedVisibility({ collapsed: true }); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testOneExtraCommandIsNotCollapsed() { + const numCommands = 6; + let extension = await startExtensionWithCommands(numCommands); + + let win = await loadShortcutsView(); + let doc = win.document; + + // The card is not expanded, since it doesn't collapse. + let card = doc.querySelector(`.card[addon-id="${extension.id}"]`); + ok(!card.hasAttribute("expanded"), "The card is not expanded"); + + // Each shortcut has a row. + let shortcutRows = card.querySelectorAll(".shortcut-row"); + is(shortcutRows.length, numCommands, `There are ${numCommands} shortcuts`); + + // There's no expand button, since it can't be expanded. + let expandButton = card.querySelector(".expand-button"); + ok(!expandButton, "There is no expand button"); + + // All of the rows are visible, to avoid a "Show 1 More" button. + for (let row of shortcutRows) { + Assert.notEqual( + getComputedStyle(row).display, + "none", + "All the rows are visible" + ); + } + + await closeView(win); + await extension.unload(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_hidden.js b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_hidden.js new file mode 100644 index 0000000000..327a99af9e --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_hidden.js @@ -0,0 +1,198 @@ +/* eslint max-len: ["error", 80] */ +"use strict"; + +async function loadShortcutsView() { + let managerWin = await open_manager(null); + managerWin.gViewController.loadView("addons://shortcuts/shortcuts"); + await wait_for_view_load(managerWin); + return managerWin.document; +} + +async function closeShortcutsView(doc) { + let managerWin = doc.defaultView.parent; + await close_manager(managerWin); +} + +async function registerAndStartExtension(mockProvider, ext) { + // Shortcuts are registered when an extension is started, so we need to load + // and start an extension. + let extension = ExtensionTestUtils.loadExtension(ext); + await extension.startup(); + + // Extensions only appear in the add-on manager when they are registered with + // the add-on manager, e.g. by passing "useAddonManager" to `loadExtension`. + // "useAddonManager" can however not be used, because the resulting add-ons + // are unsigned, and only add-ons with privileged signatures can be hidden. + mockProvider.createAddons([ + { + id: extension.id, + name: ext.manifest.name, + type: "extension", + version: "1", + // We use MockProvider because the "hidden" property cannot + // be set when "useAddonManager" is passed to loadExtension. + hidden: ext.manifest.hidden, + isSystem: ext.isSystem, + }, + ]); + return extension; +} + +function getShortcutCard(doc, extension) { + return doc.querySelector(`.shortcut[addon-id="${extension.id}"]`); +} + +function getShortcutByName(doc, extension, name) { + let card = getShortcutCard(doc, extension); + return card && card.querySelector(`.shortcut-input[name="${name}"]`); +} + +function getNoShortcutListItem(doc, extension) { + let { id } = extension; + let li = doc.querySelector(`.shortcuts-no-commands-list [addon-id="${id}"]`); + return li && li.textContent; +} + +add_task(async function extension_with_shortcuts() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "shortcut addon", + commands: { + theShortcut: {}, + }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + let doc = await loadShortcutsView(); + + ok( + getShortcutByName(doc, extension, "theShortcut"), + "Extension with shortcuts should have a card" + ); + is( + getNoShortcutListItem(doc, extension), + null, + "Extension with shortcuts should not be listed" + ); + + await closeShortcutsView(doc); + await extension.unload(); +}); + +add_task(async function extension_without_shortcuts() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "no shortcut addon", + }, + useAddonManager: "temporary", + }); + await extension.startup(); + let doc = await loadShortcutsView(); + + is( + getShortcutCard(doc, extension), + null, + "Extension without shortcuts should not have a card" + ); + is( + getNoShortcutListItem(doc, extension), + "no shortcut addon", + "The add-on's name is set in the list" + ); + + await closeShortcutsView(doc); + await extension.unload(); +}); + +// Hidden add-ons without shortcuts should be hidden, +// but their card should be shown if there is a shortcut. +add_task(async function hidden_extension() { + let mockProvider = new MockProvider(); + let hiddenExt1 = await registerAndStartExtension(mockProvider, { + manifest: { + name: "hidden with shortcuts", + hidden: true, + commands: { + hiddenShortcut: {}, + }, + }, + }); + let hiddenExt2 = await registerAndStartExtension(mockProvider, { + manifest: { + name: "hidden without shortcuts", + hidden: true, + }, + }); + + let doc = await loadShortcutsView(); + + ok( + getShortcutByName(doc, hiddenExt1, "hiddenShortcut"), + "Hidden extension with shortcuts should have a card" + ); + + is( + getShortcutCard(doc, hiddenExt2), + null, + "Hidden extension without shortcuts should not have a card" + ); + is( + getNoShortcutListItem(doc, hiddenExt2), + null, + "Hidden extension without shortcuts should not be listed" + ); + + await closeShortcutsView(doc); + await hiddenExt1.unload(); + await hiddenExt2.unload(); + + mockProvider.unregister(); +}); + +add_task(async function system_addons_and_shortcuts() { + let mockProvider = new MockProvider(); + let systemExt1 = await registerAndStartExtension(mockProvider, { + isSystem: true, + manifest: { + name: "system with shortcuts", + // In practice, all XPIStateLocations with isSystem=true also have + // isBuiltin=true, which implies that hidden=true as well. + hidden: true, + commands: { + systemShortcut: {}, + }, + }, + }); + let systemExt2 = await registerAndStartExtension(mockProvider, { + isSystem: true, + manifest: { + name: "system without shortcuts", + hidden: true, + }, + }); + + let doc = await loadShortcutsView(); + + ok( + getShortcutByName(doc, systemExt1, "systemShortcut"), + "System add-on with shortcut should have a card" + ); + + is( + getShortcutCard(doc, systemExt2), + null, + "System add-on without shortcut should not have a card" + ); + is( + getNoShortcutListItem(doc, systemExt2), + null, + "System add-on without shortcuts should not be listed" + ); + + await closeShortcutsView(doc); + await systemExt1.unload(); + await systemExt2.unload(); + + mockProvider.unregister(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_remove.js b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_remove.js new file mode 100644 index 0000000000..259c10d730 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_remove.js @@ -0,0 +1,180 @@ +"use strict"; + +async function loadShortcutsView() { + let managerWin = await open_manager(null); + managerWin.gViewController.loadView("addons://shortcuts/shortcuts"); + await wait_for_view_load(managerWin); + return managerWin.document; +} + +async function closeShortcutsView(doc) { + let managerWin = doc.defaultView.parent; + await close_manager(managerWin); +} + +function getShortcutCard(doc, extension) { + return doc.querySelector(`.shortcut[addon-id="${extension.id}"]`); +} + +function getShortcutByName(doc, extension, name) { + let card = getShortcutCard(doc, extension); + return card && card.querySelector(`.shortcut-input[name="${name}"]`); +} + +async function waitForShortcutSet(input, expected) { + let doc = input.ownerDocument; + await BrowserTestUtils.waitForCondition( + () => input.getAttribute("shortcut") == expected, + `Shortcut should be set to ${JSON.stringify(expected)}` + ); + Assert.notEqual(doc.activeElement, input, "The input is no longer focused"); + checkHasRemoveButton(input, expected !== ""); +} + +function removeButtonForInput(input) { + let removeButton = input.parentNode.querySelector(".shortcut-remove-button"); + ok(removeButton, "has remove button"); + ok( + removeButton.hasAttribute("aria-label"), + "The remove button has an accessible name" + ); + return removeButton; +} + +function checkHasRemoveButton(input, expected) { + let removeButton = removeButtonForInput(input); + let visibility = input.ownerGlobal.getComputedStyle(removeButton).visibility; + if (expected) { + is(visibility, "visible", "Remove button should be visible"); + } else { + is(visibility, "hidden", "Remove button should be hidden"); + } +} + +add_task(async function test_remove_shortcut() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + commands: { + commandEmpty: {}, + commandOne: { + suggested_key: { default: "Shift+Alt+1" }, + }, + commandTwo: { + suggested_key: { default: "Shift+Alt+2" }, + }, + }, + }, + background() { + browser.commands.onCommand.addListener(commandName => { + browser.test.sendMessage("oncommand", commandName); + }); + }, + useAddonManager: "temporary", + }); + + await extension.startup(); + + let doc = await loadShortcutsView(); + + let input = getShortcutByName(doc, extension, "commandOne"); + + checkHasRemoveButton(input, true); + + // First: Verify that Shift-Del is not valid, but doesn't do anything. + input.focus(); + EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true }); + let errorElem = doc.querySelector(".error-message"); + is(errorElem.style.visibility, "visible", "Expected error message"); + let errorId = doc.l10n.getAttributes( + errorElem.querySelector(".error-message-label") + ).id; + if (AppConstants.platform == "macosx") { + is(errorId, "shortcuts-modifier-mac", "Shift-Del is not a valid shortcut"); + } else { + is(errorId, "shortcuts-modifier-other", "Shift-Del isn't a valid shortcut"); + } + checkHasRemoveButton(input, true); + + // Now, verify that the original shortcut still works. + EventUtils.synthesizeKey("KEY_Escape"); + Assert.notEqual(doc.activeElement, input, "The input is no longer focused"); + is(errorElem.style.visibility, "hidden", "The error is hidden"); + + EventUtils.synthesizeKey("1", { altKey: true, shiftKey: true }); + await extension.awaitMessage("oncommand"); + + // Alt-Shift-Del is a valid shortcut. + input.focus(); + EventUtils.synthesizeKey("KEY_Delete", { altKey: true, shiftKey: true }); + await waitForShortcutSet(input, "Alt+Shift+Delete"); + EventUtils.synthesizeKey("KEY_Delete", { altKey: true, shiftKey: true }); + await extension.awaitMessage("oncommand"); + + // Del without modifiers should clear the shortcut. + input.focus(); + EventUtils.synthesizeKey("KEY_Delete"); + await waitForShortcutSet(input, ""); + // Trigger the shortcuts that were originally associated with commandOne, + // and then trigger commandTwo. The extension should only see commandTwo. + EventUtils.synthesizeKey("1", { altKey: true, shiftKey: true }); + EventUtils.synthesizeKey("KEY_Delete", { altKey: true, shiftKey: true }); + EventUtils.synthesizeKey("2", { altKey: true, shiftKey: true }); + is( + await extension.awaitMessage("oncommand"), + "commandTwo", + "commandOne should be disabled, commandTwo should still be enabled" + ); + + // Set a shortcut where the default was not set. + let inputEmpty = getShortcutByName(doc, extension, "commandEmpty"); + is(inputEmpty.getAttribute("shortcut"), "", "Empty shortcut by default"); + checkHasRemoveButton(input, false); + inputEmpty.focus(); + EventUtils.synthesizeKey("3", { altKey: true, shiftKey: true }); + await waitForShortcutSet(inputEmpty, "Alt+Shift+3"); + EventUtils.synthesizeKey("3", { altKey: true, shiftKey: true }); + await extension.awaitMessage("oncommand"); + // Clear shortcut. + inputEmpty.focus(); + EventUtils.synthesizeKey("KEY_Delete"); + await waitForShortcutSet(inputEmpty, ""); + EventUtils.synthesizeKey("3", { altKey: true, shiftKey: true }); + EventUtils.synthesizeKey("2", { altKey: true, shiftKey: true }); + is( + await extension.awaitMessage("oncommand"), + "commandTwo", + "commandEmpty should be disabled, commandTwo should still be enabled" + ); + + // Now verify that the Backspace button does the same thing as Delete. + inputEmpty.focus(); + EventUtils.synthesizeKey("3", { altKey: true, shiftKey: true }); + await waitForShortcutSet(inputEmpty, "Alt+Shift+3"); + inputEmpty.focus(); + EventUtils.synthesizeKey("KEY_Backspace"); + await waitForShortcutSet(input, ""); + EventUtils.synthesizeKey("3", { altKey: true, shiftKey: true }); + EventUtils.synthesizeKey("2", { altKey: true, shiftKey: true }); + is( + await extension.awaitMessage("oncommand"), + "commandTwo", + "commandEmpty should be disabled again by Backspace" + ); + + // Check that the remove button works as expected. + let inputTwo = getShortcutByName(doc, extension, "commandTwo"); + is(inputTwo.getAttribute("shortcut"), "Shift+Alt+2", "initial shortcut"); + checkHasRemoveButton(inputTwo, true); + removeButtonForInput(inputTwo).click(); + is(inputTwo.getAttribute("shortcut"), "", "cleared shortcut"); + checkHasRemoveButton(inputTwo, false); + Assert.notEqual( + doc.activeElement, + inputTwo, + "input of removed shortcut is not focused" + ); + + await closeShortcutsView(doc); + + await extension.unload(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_menu_button_accessibility.js b/toolkit/mozapps/extensions/test/browser/browser_menu_button_accessibility.js new file mode 100644 index 0000000000..a602d84999 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_menu_button_accessibility.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function testOpenMenu(btn, method) { + let shown = BrowserTestUtils.waitForEvent(btn.ownerGlobal, "shown", true); + await method(); + await shown; + is(btn.getAttribute("aria-expanded"), "true", "expanded when open"); +} + +async function testCloseMenu(btn, method) { + let hidden = BrowserTestUtils.waitForEvent(btn.ownerGlobal, "hidden", true); + await method(); + await hidden; + is(btn.getAttribute("aria-expanded"), "false", "not expanded when closed"); +} + +async function testButton(btn) { + let win = btn.ownerGlobal; + + is(btn.getAttribute("aria-haspopup"), "menu", "it has a menu"); + is(btn.getAttribute("aria-expanded"), "false", "not expanded"); + + info("Test open/close with mouse"); + await testOpenMenu(btn, () => { + EventUtils.synthesizeMouseAtCenter(btn, {}, win); + }); + await testCloseMenu(btn, () => { + let spacer = win.document.querySelector(".main-heading .spacer"); + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a non-interactive element to dismiss the + // opened menu with a mouse which can be done by assistive technology and + // keyboard by pressing `Esc` key, this rule check shall be ignored by + // a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + EventUtils.synthesizeMouseAtCenter(spacer, {}, win); + AccessibilityUtils.resetEnv(); + }); + + info("Test open/close with keyboard"); + await testOpenMenu(btn, async () => { + btn.focus(); + EventUtils.synthesizeKey(" ", {}, win); + }); + await testCloseMenu(btn, () => { + EventUtils.synthesizeKey("Escape", {}, win); + }); +} + +add_task(async function testPageOptionsMenuButton() { + let win = await loadInitialView("extension"); + + await testButton( + win.document.querySelector(".page-options-menu .more-options-button") + ); + + await closeView(win); +}); + +add_task(async function testCardMoreOptionsButton() { + let id = "more-options-button@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + let card = getAddonCard(win, id); + + info("Check list page"); + await testButton(card.querySelector(".more-options-button")); + + let viewLoaded = waitForViewLoad(win); + + EventUtils.synthesizeMouseAtCenter( + card.querySelector(".addon-name-link"), + {}, + win + ); + await viewLoaded; + + info("Check detail page"); + card = getAddonCard(win, id); + await testButton(card.querySelector(".more-options-button")); + + await closeView(win); + await extension.unload(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_page_accessibility.js b/toolkit/mozapps/extensions/test/browser/browser_page_accessibility.js new file mode 100644 index 0000000000..e049cbd618 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_page_accessibility.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testPageTitle() { + let win = await loadInitialView("extension"); + let title = win.document.querySelector("title"); + is( + win.document.l10n.getAttributes(title).id, + "addons-page-title", + "The page title is set" + ); + await closeView(win); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_page_options_install_addon.js b/toolkit/mozapps/extensions/test/browser/browser_page_options_install_addon.js new file mode 100644 index 0000000000..5007731927 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_page_options_install_addon.js @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests bug 567127 - Add install button to the add-ons manager + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +async function checkInstallConfirmation(...names) { + let notificationCount = 0; + let observer = { + observe(aSubject, aTopic, aData) { + var installInfo = aSubject.wrappedJSObject; + isnot( + installInfo.browser, + null, + "Notification should have non-null browser" + ); + Assert.deepEqual( + installInfo.installs[0].installTelemetryInfo, + { + source: "about:addons", + method: "install-from-file", + }, + "Got the expected installTelemetryInfo" + ); + notificationCount++; + }, + }; + Services.obs.addObserver(observer, "addon-install-started"); + + let results = []; + + let promise = promisePopupNotificationShown("addon-webext-permissions"); + for (let i = 0; i < names.length; i++) { + let panel = await promise; + let name = panel.getAttribute("name"); + results.push(name); + + info(`Saw install for ${name}`); + if (results.length < names.length) { + info( + `Waiting for installs for ${names.filter(n => !results.includes(n))}` + ); + + promise = promisePopupNotificationShown("addon-webext-permissions"); + } + panel.secondaryButton.click(); + } + + Assert.deepEqual(results.sort(), names.sort(), "Got expected installs"); + + is( + notificationCount, + names.length, + `Saw ${names.length} addon-install-started notification` + ); + Services.obs.removeObserver(observer, "addon-install-started"); +} + +add_task(async function test_install_from_file() { + let win = await loadInitialView("extension"); + + var filePaths = [ + get_addon_file_url("browser_dragdrop1.xpi"), + get_addon_file_url("browser_dragdrop2.xpi"), + ]; + for (let uri of filePaths) { + Assert.notEqual(uri.file, null, `Should have file for ${uri.spec}`); + ok(uri.file instanceof Ci.nsIFile, `Should have nsIFile for ${uri.spec}`); + } + MockFilePicker.setFiles(filePaths.map(aPath => aPath.file)); + + // Set handler that executes the core test after the window opens, + // and resolves the promise when the window closes + let pInstallURIClosed = checkInstallConfirmation( + "Drag Drop test 1", + "Drag Drop test 2" + ); + + win.document + .querySelector('#page-options [action="install-from-file"]') + .click(); + + await pInstallURIClosed; + + MockFilePicker.cleanup(); + await closeView(win); +}); + +add_task(async function test_install_disabled() { + let win = await loadInitialView("extension"); + let doc = win.document; + + let pageOptionsMenu = doc.querySelector("addon-page-options panel-list"); + + function openPageOptions() { + let opened = BrowserTestUtils.waitForEvent(pageOptionsMenu, "shown"); + pageOptionsMenu.open = true; + return opened; + } + + function closePageOptions() { + let closed = BrowserTestUtils.waitForEvent(pageOptionsMenu, "hidden"); + pageOptionsMenu.open = false; + return closed; + } + + await openPageOptions(); + let installButton = doc.querySelector('[action="install-from-file"]'); + ok(!installButton.hidden, "The install button is shown"); + await closePageOptions(); + + await SpecialPowers.pushPrefEnv({ set: [[PREF_XPI_ENABLED, false]] }); + + await openPageOptions(); + ok(installButton.hidden, "The install button is now hidden"); + await closePageOptions(); + + await SpecialPowers.popPrefEnv(); + + await openPageOptions(); + ok(!installButton.hidden, "The install button is shown again"); + await closePageOptions(); + + await closeView(win); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_page_options_updates.js b/toolkit/mozapps/extensions/test/browser/browser_page_options_updates.js new file mode 100644 index 0000000000..bd7572a061 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_page_options_updates.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Make sure we don't accidentally start a background update while the prefs +// are enabled. +disableBackgroundUpdateTimer(); +registerCleanupFunction(() => { + enableBackgroundUpdateTimer(); +}); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const PREF_UPDATE_ENABLED = "extensions.update.enabled"; +const PREF_AUTOUPDATE_DEFAULT = "extensions.update.autoUpdateDefault"; + +add_task(async function testUpdateAutomaticallyButton() { + SpecialPowers.pushPrefEnv({ + set: [ + [PREF_UPDATE_ENABLED, true], + [PREF_AUTOUPDATE_DEFAULT, true], + ], + }); + + let win = await loadInitialView("extension"); + + let toggleAutomaticButton = win.document.querySelector( + '#page-options [action="set-update-automatically"]' + ); + + info("Verify the checked state reflects the update state"); + ok(toggleAutomaticButton.checked, "Automatic updates button is checked"); + + AddonManager.autoUpdateDefault = false; + ok(!toggleAutomaticButton.checked, "Automatic updates button is unchecked"); + + AddonManager.autoUpdateDefault = true; + ok(toggleAutomaticButton.checked, "Automatic updates button is re-checked"); + + info("Verify that clicking the button changes the update state"); + ok(AddonManager.autoUpdateDefault, "Auto updates are default"); + ok(AddonManager.updateEnabled, "Updates are enabled"); + + toggleAutomaticButton.click(); + ok(!AddonManager.autoUpdateDefault, "Auto updates are disabled"); + ok(AddonManager.updateEnabled, "Updates are enabled"); + + toggleAutomaticButton.click(); + ok(AddonManager.autoUpdateDefault, "Auto updates are enabled again"); + ok(AddonManager.updateEnabled, "Updates are enabled"); + + await closeView(win); +}); + +add_task(async function testResetUpdateStates() { + let id = "update-state@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "permanent", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + let resetStateButton = win.document.querySelector( + '#page-options [action="reset-update-states"]' + ); + + info("Changing add-on update state"); + let addon = await AddonManager.getAddonByID(id); + + let setAddonUpdateState = async updateState => { + let changed = AddonTestUtils.promiseAddonEvent("onPropertyChanged"); + addon.applyBackgroundUpdates = updateState; + await changed; + let addonState = addon.applyBackgroundUpdates; + is(addonState, updateState, `Add-on updates are ${updateState}`); + }; + + await setAddonUpdateState(AddonManager.AUTOUPDATE_DISABLE); + + let propertyChanged = AddonTestUtils.promiseAddonEvent("onPropertyChanged"); + resetStateButton.click(); + await propertyChanged; + is( + addon.applyBackgroundUpdates, + AddonManager.AUTOUPDATE_DEFAULT, + "Add-on is reset to default updates" + ); + + await setAddonUpdateState(AddonManager.AUTOUPDATE_ENABLE); + + propertyChanged = AddonTestUtils.promiseAddonEvent("onPropertyChanged"); + resetStateButton.click(); + await propertyChanged; + is( + addon.applyBackgroundUpdates, + AddonManager.AUTOUPDATE_DEFAULT, + "Add-on is reset to default updates again" + ); + + info("Check the label on the button as the global state changes"); + is( + win.document.l10n.getAttributes(resetStateButton).id, + "addon-updates-reset-updates-to-automatic", + "The reset button label says it resets to automatic" + ); + + info("Disable auto updating globally"); + AddonManager.autoUpdateDefault = false; + + is( + win.document.l10n.getAttributes(resetStateButton).id, + "addon-updates-reset-updates-to-manual", + "The reset button label says it resets to manual" + ); + + await closeView(win); + await extension.unload(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_permission_prompt.js b/toolkit/mozapps/extensions/test/browser/browser_permission_prompt.js new file mode 100644 index 0000000000..d58eb8c027 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_permission_prompt.js @@ -0,0 +1,178 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* + * Test Permission Popup for Sideloaded Extensions. + */ +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const ADDON_ID = "addon1@test.mozilla.org"; +const CUSTOM_THEME_ID = "theme1@test.mozilla.org"; +const DEFAULT_THEME_ID = "default-theme@mozilla.org"; + +AddonTestUtils.initMochitest(this); + +function assertDisabledSideloadedExtensionElement(managerWindow, addonElement) { + const doc = addonElement.ownerDocument; + const toggleDisabled = addonElement.querySelector( + '[action="toggle-disabled"]' + ); + is( + doc.l10n.getAttributes(toggleDisabled).id, + "extension-enable-addon-button-label", + "Addon toggle-disabled action has the enable label" + ); + ok(!toggleDisabled.checked, "toggle-disable isn't checked"); +} + +function assertEnabledSideloadedExtensionElement(managerWindow, addonElement) { + const doc = addonElement.ownerDocument; + const toggleDisabled = addonElement.querySelector( + '[action="toggle-disabled"]' + ); + is( + doc.l10n.getAttributes(toggleDisabled).id, + "extension-enable-addon-button-label", + "Addon toggle-disabled action has the enable label" + ); + ok(!toggleDisabled.checked, "toggle-disable isn't checked"); +} + +function clickEnableExtension(addonElement) { + addonElement.querySelector('[action="toggle-disabled"]').click(); +} + +// Test for bug 1647931 +// Install a theme, enable it and then enable the default theme again +add_task(async function test_theme_enable() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["xpinstall.signatures.required", false], + ["extensions.autoDisableScopes", 15], + ["extensions.ui.ignoreUnsigned", true], + ], + }); + + let theme = { + manifest: { + browser_specific_settings: { gecko: { id: CUSTOM_THEME_ID } }, + name: "Theme 1", + theme: { + colors: { + frame: "#000000", + tab_background_text: "#ffffff", + }, + }, + }, + }; + + let xpi = AddonTestUtils.createTempWebExtensionFile(theme); + await AddonTestUtils.manuallyInstall(xpi); + + let changePromise = new Promise(resolve => + ExtensionsUI.once("change", resolve) + ); + ExtensionsUI._checkForSideloaded(); + await changePromise; + + // enable fresh installed theme + let manager = await open_manager("addons://list/theme"); + let customTheme = getAddonCard(manager, CUSTOM_THEME_ID); + clickEnableExtension(customTheme); + + // enable default theme again + let defaultTheme = getAddonCard(manager, DEFAULT_THEME_ID); + clickEnableExtension(defaultTheme); + + let addon = await AddonManager.getAddonByID(CUSTOM_THEME_ID); + await close_manager(manager); + await addon.uninstall(); +}); + +// Loading extension by sideloading method +add_task(async function test_sideloaded_extension_permissions_prompt() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["xpinstall.signatures.required", false], + ["extensions.autoDisableScopes", 15], + ["extensions.ui.ignoreUnsigned", true], + ], + }); + + let options = { + manifest: { + browser_specific_settings: { gecko: { id: ADDON_ID } }, + name: "Test 1", + permissions: ["history", "https://*/*"], + icons: { 64: "foo-icon.png" }, + }, + }; + + let xpi = AddonTestUtils.createTempWebExtensionFile(options); + await AddonTestUtils.manuallyInstall(xpi); + + let changePromise = new Promise(resolve => + ExtensionsUI.once("change", resolve) + ); + ExtensionsUI._checkForSideloaded(); + await changePromise; + + // Test click event on permission cancel option. + let manager = await open_manager("addons://list/extension"); + let addon = getAddonCard(manager, ADDON_ID); + + Assert.notEqual(addon, null, "Found sideloaded addon in about:addons"); + + assertDisabledSideloadedExtensionElement(manager, addon); + + let popupPromise = promisePopupNotificationShown("addon-webext-permissions"); + clickEnableExtension(addon); + let panel = await popupPromise; + + ok(PopupNotifications.isPanelOpen, "Permission popup should be visible"); + panel.secondaryButton.click(); + ok( + !PopupNotifications.isPanelOpen, + "Permission popup should be closed / closing" + ); + + addon = await AddonManager.getAddonByID(ADDON_ID); + ok( + !addon.seen, + "Seen flag should remain false after permissions are refused" + ); + + // Test click event on permission accept option. + addon = getAddonCard(manager, ADDON_ID); + Assert.notEqual(addon, null, "Found sideloaded addon in about:addons"); + + assertEnabledSideloadedExtensionElement(manager, addon); + + popupPromise = promisePopupNotificationShown("addon-webext-permissions"); + clickEnableExtension(addon); + panel = await popupPromise; + + ok(PopupNotifications.isPanelOpen, "Permission popup should be visible"); + + let notificationPromise = acceptAppMenuNotificationWhenShown( + "addon-installed", + ADDON_ID + ); + + panel.button.click(); + ok( + !PopupNotifications.isPanelOpen, + "Permission popup should be closed / closing" + ); + await notificationPromise; + + addon = await AddonManager.getAddonByID(ADDON_ID); + ok(addon.seen, "Seen flag should be true after permissions are accepted"); + + ok(!PopupNotifications.isPanelOpen, "Permission popup should not be visible"); + + await close_manager(manager); + await addon.uninstall(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_reinstall.js b/toolkit/mozapps/extensions/test/browser/browser_reinstall.js new file mode 100644 index 0000000000..c0eb7d139a --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_reinstall.js @@ -0,0 +1,277 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that upgrading bootstrapped add-ons behaves correctly while the +// manager is open + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const ID = "reinstall@tests.mozilla.org"; +const testIdSuffix = "@tests.mozilla.org"; + +let gManagerWindow, xpi1, xpi2; + +function htmlDoc() { + return gManagerWindow.document; +} + +function get_list_item_count() { + return htmlDoc().querySelectorAll(`addon-card[addon-id$="${testIdSuffix}"]`) + .length; +} + +function removeItem(item) { + let button = item.querySelector('[action="remove"]'); + button.click(); +} + +function hasPendingMessage(item, msg) { + let messageBar = htmlDoc().querySelector( + `moz-message-bar[addon-id="${item.addon.id}"` + ); + is_element_visible(messageBar, msg); +} + +async function install_addon(xpi) { + let install = await AddonManager.getInstallForFile( + xpi, + "application/x-xpinstall" + ); + return install.install(); +} + +async function check_addon(aAddon, aVersion) { + is(get_list_item_count(), 1, "Should be one item in the list"); + is(aAddon.version, aVersion, "Add-on should have the right version"); + + let item = getAddonCard(gManagerWindow, ID); + ok(!!item, "Should see the add-on in the list"); + + // Force XBL to apply + item.clientTop; + + let { version } = await get_tooltip_info(item, gManagerWindow); + is(version, aVersion, "Version should be correct"); + + const l10nAttrs = item.ownerDocument.l10n.getAttributes(item.addonNameEl); + if (aAddon.userDisabled) { + Assert.deepEqual( + l10nAttrs, + { id: "addon-name-disabled", args: { name: aAddon.name } }, + "localized addon name is marked as disabled" + ); + } else { + Assert.deepEqual( + l10nAttrs, + { id: null, args: null }, + "localized addon name is not marked as disabled" + ); + } +} + +async function wait_for_addon_item_added(addonId) { + await BrowserTestUtils.waitForEvent( + htmlDoc().querySelector("addon-list"), + "add" + ); + const item = getAddonCard(gManagerWindow, addonId); + ok(item, `Found addon card for ${addonId}`); +} + +async function wait_for_addon_item_removed(addonId) { + await BrowserTestUtils.waitForEvent( + htmlDoc().querySelector("addon-list"), + "remove" + ); + const item = getAddonCard(gManagerWindow, addonId); + ok(!item, `There shouldn't be an addon card for ${addonId}`); +} + +function wait_for_addon_item_updated(addonId) { + return BrowserTestUtils.waitForEvent( + getAddonCard(gManagerWindow, addonId), + "update" + ); +} + +// Install version 1 then upgrade to version 2 with the manager open +async function test_upgrade_v1_to_v2() { + let promiseItemAdded = wait_for_addon_item_added(ID); + await install_addon(xpi1); + await promiseItemAdded; + + let addon = await promiseAddonByID(ID); + await check_addon(addon, "1.0"); + ok(!addon.userDisabled, "Add-on should not be disabled"); + + let promiseItemUpdated = wait_for_addon_item_updated(ID); + await install_addon(xpi2); + await promiseItemUpdated; + + addon = await promiseAddonByID(ID); + await check_addon(addon, "2.0"); + ok(!addon.userDisabled, "Add-on should not be disabled"); + + let promiseItemRemoved = wait_for_addon_item_removed(ID); + await addon.uninstall(); + await promiseItemRemoved; + + is(get_list_item_count(), 0, "Should be no items in the list"); +} + +// Install version 1 mark it as disabled then upgrade to version 2 with the +// manager open +async function test_upgrade_disabled_v1_to_v2() { + let promiseItemAdded = wait_for_addon_item_added(ID); + await install_addon(xpi1); + await promiseItemAdded; + + let promiseItemUpdated = wait_for_addon_item_updated(ID); + let addon = await promiseAddonByID(ID); + await addon.disable(); + await promiseItemUpdated; + + await check_addon(addon, "1.0"); + ok(addon.userDisabled, "Add-on should be disabled"); + + promiseItemUpdated = wait_for_addon_item_updated(ID); + await install_addon(xpi2); + await promiseItemUpdated; + + addon = await promiseAddonByID(ID); + await check_addon(addon, "2.0"); + ok(addon.userDisabled, "Add-on should be disabled"); + + let promiseItemRemoved = wait_for_addon_item_removed(ID); + await addon.uninstall(); + await promiseItemRemoved; + + is(get_list_item_count(), 0, "Should be no items in the list"); +} + +// Install version 1 click the remove button and then upgrade to version 2 with +// the manager open +async function test_upgrade_pending_uninstall_v1_to_v2() { + let promiseItemAdded = wait_for_addon_item_added(ID); + await install_addon(xpi1); + await promiseItemAdded; + + let addon = await promiseAddonByID(ID); + await check_addon(addon, "1.0"); + ok(!addon.userDisabled, "Add-on should not be disabled"); + + let item = getAddonCard(gManagerWindow, ID); + + let promiseItemRemoved = wait_for_addon_item_removed(ID); + removeItem(item); + + // Force XBL to apply + item.clientTop; + + await promiseItemRemoved; + + ok( + !!(addon.pendingOperations & AddonManager.PENDING_UNINSTALL), + "Add-on should be pending uninstall" + ); + hasPendingMessage(item, "Pending message should be visible"); + + promiseItemAdded = wait_for_addon_item_added(ID); + await install_addon(xpi2); + await promiseItemAdded; + + addon = await promiseAddonByID(ID); + await check_addon(addon, "2.0"); + ok(!addon.userDisabled, "Add-on should not be disabled"); + + promiseItemRemoved = wait_for_addon_item_removed(ID); + await addon.uninstall(); + await promiseItemRemoved; + + is(get_list_item_count(), 0, "Should be no items in the list"); +} + +// Install version 1, disable it, click the remove button and then upgrade to +// version 2 with the manager open +async function test_upgrade_pending_uninstall_disabled_v1_to_v2() { + let promiseItemAdded = wait_for_addon_item_added(ID); + await install_addon(xpi1); + await promiseItemAdded; + + let promiseItemUpdated = wait_for_addon_item_updated(ID); + let addon = await promiseAddonByID(ID); + await addon.disable(); + await promiseItemUpdated; + + await check_addon(addon, "1.0"); + ok(addon.userDisabled, "Add-on should be disabled"); + + let item = getAddonCard(gManagerWindow, ID); + + let promiseItemRemoved = wait_for_addon_item_removed(ID); + removeItem(item); + + // Force XBL to apply + item.clientTop; + + await promiseItemRemoved; + ok( + !!(addon.pendingOperations & AddonManager.PENDING_UNINSTALL), + "Add-on should be pending uninstall" + ); + hasPendingMessage(item, "Pending message should be visible"); + + promiseItemAdded = wait_for_addon_item_added(ID); + await install_addon(xpi2); + addon = await promiseAddonByID(ID); + + await promiseItemAdded; + await check_addon(addon, "2.0"); + ok(addon.userDisabled, "Add-on should be disabled"); + + promiseItemRemoved = wait_for_addon_item_removed(ID); + await addon.uninstall(); + await promiseItemRemoved; + + is(get_list_item_count(), 0, "Should be no items in the list"); +} + +add_setup(async function () { + xpi1 = await AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version: "1.0", + browser_specific_settings: { gecko: { id: ID } }, + }, + }); + + xpi2 = await AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version: "2.0", + browser_specific_settings: { gecko: { id: ID } }, + }, + }); + + // Accept all prompts. + mockPromptService()._response = 0; +}); + +add_task(async function test_upgrades() { + // Close existing about:addons tab if a test failure has + // prevented it from being closed. + if (gManagerWindow) { + await close_manager(gManagerWindow); + } + + gManagerWindow = await open_manager("addons://list/extension"); + + await test_upgrade_v1_to_v2(); + await test_upgrade_disabled_v1_to_v2(); + await test_upgrade_pending_uninstall_v1_to_v2(); + await test_upgrade_pending_uninstall_disabled_v1_to_v2(); + + await close_manager(gManagerWindow); + gManagerWindow = null; +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_shortcuts_duplicate_check.js b/toolkit/mozapps/extensions/test/browser/browser_shortcuts_duplicate_check.js new file mode 100644 index 0000000000..912ce8d62f --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_shortcuts_duplicate_check.js @@ -0,0 +1,262 @@ +"use strict"; + +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally( + /Message manager disconnected/ +); + +async function loadShortcutsView() { + let win = await loadInitialView("extension"); + + // There should be a manage shortcuts link. + let shortcutsLink = win.document.querySelector('[action="manage-shortcuts"]'); + + // Open the shortcuts view. + let loaded = waitForViewLoad(win); + shortcutsLink.click(); + await loaded; + + return win; +} + +add_task(async function testDuplicateShortcutsWarnings() { + let duplicateCommands = { + commandOne: { + suggested_key: { default: "Shift+Alt+1" }, + }, + commandTwo: { + description: "Command Two!", + suggested_key: { default: "Shift+Alt+2" }, + }, + }; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + commands: duplicateCommands, + name: "Extension 1", + }, + background() { + browser.test.sendMessage("ready"); + }, + useAddonManager: "temporary", + }); + + await extension.startup(); + await extension.awaitMessage("ready"); + + let extension2 = ExtensionTestUtils.loadExtension({ + manifest: { + commands: { + ...duplicateCommands, + commandThree: { + description: "Command Three!", + suggested_key: { default: "Shift+Alt+3" }, + }, + }, + name: "Extension 2", + }, + background() { + browser.test.sendMessage("ready"); + }, + useAddonManager: "temporary", + }); + + await extension2.startup(); + await extension2.awaitMessage("ready"); + + let win = await loadShortcutsView(); + let doc = win.document; + + let warningBars = doc.querySelectorAll("moz-message-bar"); + // Ensure warning messages are shown for each duplicate shorctut. + is( + warningBars.length, + Object.keys(duplicateCommands).length, + "There is a warning message bar for each duplicate shortcut" + ); + + // Ensure warning messages are correct with correct shortcuts. + let count = 1; + for (let warning of warningBars) { + let l10nAttrs = doc.l10n.getAttributes(warning); + await TestUtils.waitForCondition(() => warning.message !== ""); + Assert.notStrictEqual( + warning.message, + "", + "Warning message attribute is set" + ); + is( + l10nAttrs.id, + "shortcuts-duplicate-warning-message2", + "Warning message l10nId is correct" + ); + Assert.deepEqual( + l10nAttrs.args, + { shortcut: `Shift+Alt+${count}` }, + "Warning message shortcut is correct" + ); + count++; + } + + ["Shift+Alt+1", "Shift+Alt+2"].forEach((shortcut, index) => { + // Ensure warning messages are correct with correct shortcuts. + let warning = warningBars[index]; + let l10nAttrs = doc.l10n.getAttributes(warning); + Assert.notStrictEqual( + warning.message, + "", + "Warning message attribute is set" + ); + is( + l10nAttrs.id, + "shortcuts-duplicate-warning-message2", + "Warning message l10nId is correct" + ); + Assert.deepEqual( + l10nAttrs.args, + { shortcut }, + "Warning message shortcut is correct" + ); + + // Check if all inputs have warning style. + let inputs = doc.querySelectorAll(`input[shortcut="${shortcut}"]`); + for (let input of inputs) { + // Check if warning error message is shown on focus. + input.focus(); + let error = doc.querySelector(".error-message"); + let label = error.querySelector(".error-message-label"); + is(error.style.visibility, "visible", "The error element is shown"); + is( + error.getAttribute("type"), + "warning", + "Duplicate shortcut has warning class" + ); + is( + label.dataset.l10nId, + "shortcuts-duplicate", + "Correct error message is shown" + ); + + // On keypress events wrning class should be removed. + EventUtils.synthesizeKey("A"); + ok( + !error.classList.contains("warning"), + "Error element should not have warning class" + ); + + input.blur(); + is( + error.style.visibility, + "hidden", + "The error element is hidden on blur" + ); + } + }); + + await closeView(win); + await extension.unload(); + await extension2.unload(); +}); + +add_task(async function testDuplicateShortcutOnMacOSCtrlKey() { + if (AppConstants.platform !== "macosx") { + ok( + true, + `Skipping macos specific test on platform ${AppConstants.platform}` + ); + return; + } + + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + name: "Extension 1", + browser_specific_settings: { + gecko: { id: "extension1@mochi.test" }, + }, + commands: { + commandOne: { + // Cover expected mac normalized shortcut on default shortcut. + suggested_key: { default: "Ctrl+Shift+1" }, + }, + commandTwo: { + suggested_key: { + default: "Alt+Shift+2", + // Cover expected mac normalized shortcut on mac-specific shortcut. + mac: "Ctrl+Shift+2", + }, + }, + }, + }, + }); + + const extension2 = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + name: "Extension 2", + browser_specific_settings: { + gecko: { id: "extension2@mochi.test" }, + }, + commands: { + anotherCommand: {}, + }, + }, + }); + + await extension.startup(); + await extension2.startup(); + + const win = await loadShortcutsView(); + const doc = win.document; + const errorEl = doc.querySelector("addon-shortcuts .error-message"); + const errorLabel = errorEl.querySelector(".error-message-label"); + + ok( + BrowserTestUtils.isHidden(errorEl), + "Expect shortcut error element to be initially hidden" + ); + + const getShortcutInput = commandName => + doc.querySelector(`input.shortcut-input[name="${commandName}"]`); + + const assertDuplicateShortcutWarning = async msg => { + await TestUtils.waitForCondition( + () => BrowserTestUtils.isVisible(errorEl), + `Wait for the shortcut-duplicate error to be visible on ${msg}` + ); + Assert.deepEqual( + document.l10n.getAttributes(errorLabel), + { + id: "shortcuts-exists", + args: { addon: "Extension 1" }, + }, + `Got the expected warning message on duplicate shortcut on ${msg}` + ); + }; + + const clearWarning = async inputEl => { + anotherCommandInput.blur(); + await TestUtils.waitForCondition( + () => BrowserTestUtils.isHidden(errorEl), + "Wait for the shortcut-duplicate error to be hidden" + ); + }; + + const anotherCommandInput = getShortcutInput("anotherCommand"); + anotherCommandInput.focus(); + EventUtils.synthesizeKey("1", { metaKey: true, shiftKey: true }); + + await assertDuplicateShortcutWarning("shortcut conflict with commandOne"); + await clearWarning(anotherCommandInput); + + anotherCommandInput.focus(); + EventUtils.synthesizeKey("2", { metaKey: true, shiftKey: true }); + + await assertDuplicateShortcutWarning("shortcut conflict with commandTwo"); + await clearWarning(anotherCommandInput); + + await closeView(win); + await extension.unload(); + await extension2.unload(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_sidebar_categories.js b/toolkit/mozapps/extensions/test/browser/browser_sidebar_categories.js new file mode 100644 index 0000000000..f391edbf34 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_sidebar_categories.js @@ -0,0 +1,166 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const THEME_ID = "default-theme@mozilla.org"; + +function assertViewHas(win, selector, msg) { + ok(win.document.querySelector(selector), msg); +} +function assertListView(win, type) { + assertViewHas(win, `addon-list[type="${type}"]`, `On ${type} list`); +} + +add_task(async function testClickingSidebarEntriesChangesView() { + let win = await loadInitialView("extension"); + let doc = win.document; + let themeCategory = doc.querySelector("#categories > [name=theme]"); + let extensionCategory = doc.querySelector("#categories > [name=extension]"); + + assertListView(win, "extension"); + + let loaded = waitForViewLoad(win); + themeCategory.click(); + await loaded; + + assertListView(win, "theme"); + + loaded = waitForViewLoad(win); + getAddonCard(win, THEME_ID).querySelector(".addon-name-link").click(); + await loaded; + + ok(!doc.querySelector("addon-list"), "No more addon-list"); + assertViewHas( + win, + `addon-card[addon-id="${THEME_ID}"][expanded]`, + "Detail view now" + ); + + loaded = waitForViewLoad(win); + EventUtils.synthesizeMouseAtCenter(themeCategory, {}, win); + await loaded; + + assertListView(win, "theme"); + + loaded = waitForViewLoad(win); + EventUtils.synthesizeMouseAtCenter(extensionCategory, {}, win); + await loaded; + + assertListView(win, "extension"); + + await closeView(win); +}); + +add_task(async function testClickingSidebarPaddingNoChange() { + let win = await loadInitialView("theme"); + let categoryUtils = new CategoryUtilities(win); + let themeCategory = categoryUtils.get("theme"); + + let loadDetailView = async () => { + let loaded = waitForViewLoad(win); + getAddonCard(win, THEME_ID).querySelector(".addon-name-link").click(); + await loaded; + + is( + win.gViewController.currentViewId, + `addons://detail/${THEME_ID}`, + "The detail view loaded" + ); + }; + + // Confirm that clicking the button directly works. + await loadDetailView(); + let loaded = waitForViewLoad(win); + EventUtils.synthesizeMouseAtCenter(themeCategory, {}, win); + await loaded; + is( + win.gViewController.currentViewId, + `addons://list/theme`, + "The detail view loaded" + ); + + // Confirm that clicking on the padding beside it does nothing. + await loadDetailView(); + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a non-interactive padding of the container + // to confirm nothing happens, thus this rule check shall be ignored by + // a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + EventUtils.synthesizeMouse(themeCategory, -5, -5, {}, win); + AccessibilityUtils.resetEnv(); + ok(!win.gViewController.isLoading, "No view is loading"); + + await closeView(win); +}); + +add_task(async function testKeyboardUsage() { + let win = await loadInitialView("extension"); + let categories = win.document.getElementById("categories"); + let extensionCategory = categories.getButtonByName("extension"); + let themeCategory = categories.getButtonByName("theme"); + let pluginCategory = categories.getButtonByName("plugin"); + + let waitForAnimationFrame = () => + new Promise(resolve => win.requestAnimationFrame(resolve)); + let sendKey = (key, e = {}) => { + EventUtils.synthesizeKey(key, e, win); + return waitForAnimationFrame(); + }; + let sendTabKey = e => sendKey("VK_TAB", e); + let isFocusInCategories = () => + categories.contains(win.document.activeElement); + + ok(!isFocusInCategories(), "Focus is not in the category list"); + + // Tab to the first focusable element. + await sendTabKey(); + + ok(isFocusInCategories(), "Focus is in the category list"); + is( + win.document.activeElement, + extensionCategory, + "The extension button is focused" + ); + + // Tab out of the categories list. + await sendTabKey(); + ok(!isFocusInCategories(), "Focus is out of the category list"); + + // Tab back into the list. + await sendTabKey({ shiftKey: true }); + is(win.document.activeElement, extensionCategory, "Back on Extensions"); + + // We're on the extension list. + assertListView(win, "extension"); + + // Switch to theme list. + let loaded = waitForViewLoad(win); + await sendKey("VK_DOWN"); + is(win.document.activeElement, themeCategory, "Themes is focused"); + await loaded; + + assertListView(win, "theme"); + + loaded = waitForViewLoad(win); + await sendKey("VK_DOWN"); + is(win.document.activeElement, pluginCategory, "Plugins is focused"); + await loaded; + + assertListView(win, "plugin"); + + await sendKey("VK_DOWN"); + is(win.document.activeElement, pluginCategory, "Plugins is still focused"); + ok(!win.gViewController.isLoading, "No view is loading"); + + loaded = waitForViewLoad(win); + await sendKey("VK_UP"); + await loaded; + loaded = waitForViewLoad(win); + await sendKey("VK_UP"); + await loaded; + is(win.document.activeElement, extensionCategory, "Extensions is focused"); + assertListView(win, "extension"); + + await closeView(win); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_sidebar_hidden_categories.js b/toolkit/mozapps/extensions/test/browser/browser_sidebar_hidden_categories.js new file mode 100644 index 0000000000..4cb641c2a0 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_sidebar_hidden_categories.js @@ -0,0 +1,214 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that the visible delay in showing the "Language" category occurs +// very minimally + +let gProvider; +let gInstall; +let gInstallProperties = [ + { + name: "Locale Category Test", + type: "locale", + }, +]; + +function installLocale() { + return new Promise(resolve => { + gInstall = gProvider.createInstalls(gInstallProperties)[0]; + gInstall.addTestListener({ + onInstallEnded(aInstall) { + gInstall.removeTestListener(this); + resolve(); + }, + }); + gInstall.install(); + }); +} + +async function checkCategory(win, category, { expectHidden, expectSelected }) { + await win.customElements.whenDefined("categories-box"); + + let categoriesBox = win.document.getElementById("categories"); + await categoriesBox.promiseRendered; + + let button = categoriesBox.getButtonByName(category); + is( + button.hidden, + expectHidden, + `${category} button is ${expectHidden ? "" : "not "}hidden` + ); + if (expectSelected !== undefined) { + is( + button.selected, + expectSelected, + `${category} button is ${expectSelected ? "" : "not "}selected` + ); + } +} + +add_setup(async function () { + gProvider = new MockProvider(); +}); + +add_task(async function testLocalesHiddenByDefault() { + gProvider.blockQueryResponses(); + + let viewLoaded = loadInitialView("extension", { + async loadCallback(win) { + await checkCategory(win, "locale", { expectHidden: true }); + gProvider.unblockQueryResponses(); + }, + }); + let win = await viewLoaded; + + await checkCategory(win, "locale", { expectHidden: true }); + + await installLocale(); + + await checkCategory(win, "locale", { + expectHidden: false, + expectSelected: false, + }); + + await closeView(win); +}); + +add_task(async function testLocalesShownWhenInstalled() { + gProvider.blockQueryResponses(); + + let viewLoaded = loadInitialView("extension", { + async loadCallback(win) { + await checkCategory(win, "locale", { + expectHidden: false, + expectSelected: false, + }); + gProvider.unblockQueryResponses(); + }, + }); + let win = await viewLoaded; + + await checkCategory(win, "locale", { + expectHidden: false, + expectSelected: false, + }); + + await closeView(win); +}); + +add_task(async function testLocalesHiddenWhenUninstalled() { + gInstall.cancel(); + gProvider.blockQueryResponses(); + + let viewLoaded = loadInitialView("extension", { + async loadCallback(win) { + await checkCategory(win, "locale", { + expectHidden: false, + expectSelected: false, + }); + gProvider.unblockQueryResponses(); + }, + }); + let win = await viewLoaded; + + await checkCategory(win, "locale", { expectHidden: true }); + + await closeView(win); +}); + +add_task(async function testLocalesHiddenWithoutDelay() { + gProvider.blockQueryResponses(); + + let viewLoaded = loadInitialView("extension", { + async loadCallback(win) { + await checkCategory(win, "locale", { expectHidden: true }); + gProvider.unblockQueryResponses(); + }, + }); + let win = await viewLoaded; + + await checkCategory(win, "locale", { expectHidden: true }); + + await closeView(win); +}); + +add_task(async function testLocalesShownAfterDelay() { + await installLocale(); + + gProvider.blockQueryResponses(); + + let viewLoaded = loadInitialView("extension", { + async loadCallback(win) { + await checkCategory(win, "locale", { expectHidden: true }); + gProvider.unblockQueryResponses(); + }, + }); + let win = await viewLoaded; + + await checkCategory(win, "locale", { + expectHidden: false, + expectSelected: false, + }); + + await closeView(win); +}); + +add_task(async function testLocalesShownIfPreviousView() { + gProvider.blockQueryResponses(); + + // Passing "locale" will set the last view to locales and open the view. + let viewLoaded = loadInitialView("locale", { + async loadCallback(win) { + await checkCategory(win, "locale", { + expectHidden: false, + expectSelected: true, + }); + gProvider.unblockQueryResponses(); + }, + }); + let win = await viewLoaded; + + await checkCategory(win, "locale", { + expectHidden: false, + expectSelected: true, + }); + + await closeView(win); +}); + +add_task(async function testLocalesHiddenIfPreviousViewAndNoLocales() { + gInstall.cancel(); + gProvider.blockQueryResponses(); + + // Passing "locale" will set the last view to locales and open the view. + let viewLoaded = loadInitialView("locale", { + async loadCallback(win) { + await checkCategory(win, "locale", { + expectHidden: false, + expectSelected: true, + }); + gProvider.unblockQueryResponses(); + }, + }); + let win = await viewLoaded; + + let categoryUtils = new CategoryUtilities(win); + + await TestUtils.waitForCondition( + () => categoryUtils.selectedCategory != "locale" + ); + + await checkCategory(win, "locale", { + expectHidden: true, + expectSelected: false, + }); + + is( + categoryUtils.getSelectedViewId(), + win.gViewController.defaultViewId, + "default view is selected" + ); + + await closeView(win); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_sidebar_restore_category.js b/toolkit/mozapps/extensions/test/browser/browser_sidebar_restore_category.js new file mode 100644 index 0000000000..4c5b1e25f0 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_sidebar_restore_category.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that the selected category is persisted across loads of the manager + +add_task(async function testCategoryRestore() { + let win = await loadInitialView("extension"); + let utils = new CategoryUtilities(win); + + // Open the plugins category + await utils.openType("plugin"); + + // Re-open the manager + await closeView(win); + win = await loadInitialView(); + utils = new CategoryUtilities(win); + + is( + utils.selectedCategory, + "plugin", + "Should have shown the plugins category" + ); + + // Open the extensions category + await utils.openType("extension"); + + // Re-open the manager + await closeView(win); + win = await loadInitialView(); + utils = new CategoryUtilities(win); + + is( + utils.selectedCategory, + "extension", + "Should have shown the extensions category" + ); + + await closeView(win); +}); + +add_task(async function testInvalidAddonType() { + let win = await loadInitialView("invalid"); + + let categoryUtils = new CategoryUtilities(win); + is( + categoryUtils.getSelectedViewId(), + win.gViewController.defaultViewId, + "default view is selected" + ); + is( + win.gViewController.currentViewId, + win.gViewController.defaultViewId, + "default view is shown" + ); + + await closeView(win); +}); + +add_task(async function testInvalidViewId() { + let win = await loadInitialView("addons://invalid/view"); + + let categoryUtils = new CategoryUtilities(win); + is( + categoryUtils.getSelectedViewId(), + win.gViewController.defaultViewId, + "default view is selected" + ); + is( + win.gViewController.currentViewId, + win.gViewController.defaultViewId, + "default view is shown" + ); + + await closeView(win); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_subframe_install.js b/toolkit/mozapps/extensions/test/browser/browser_subframe_install.js new file mode 100644 index 0000000000..e9e8c73728 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_subframe_install.js @@ -0,0 +1,234 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const XPI_URL = `${SECURE_TESTROOT}../xpinstall/amosigned.xpi`; + +AddonTestUtils.initMochitest(this); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.install.requireBuiltInCerts", false]], + }); + + registerCleanupFunction(async () => { + await SpecialPowers.popPrefEnv(); + }); +}); + +function testSubframeInstallOnNavigation({ + topFrameURL, + midFrameURL, + bottomFrameURL, + xpiURL, + assertFn, +}) { + return BrowserTestUtils.withNewTab(topFrameURL, async browser => { + await SpecialPowers.pushPrefEnv({ + // Relax the user input requirements while running this test. + set: [["xpinstall.userActivation.required", false]], + }); + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: [`${midFrameURL}*`], + js: ["createFrame.js"], + all_frames: true, + }, + { + matches: [`${bottomFrameURL}*`], + js: ["installByNavigatingToXPIURL.js"], + all_frames: true, + }, + ], + }, + files: { + "createFrame.js": `(function(frameURL) { + browser.test.log("Executing createFrame.js on " + window.location.href); + const frame = document.createElement("iframe"); + frame.src = frameURL; + document.body.appendChild(frame); + })("${bottomFrameURL}")`, + + "installByNavigatingToXPIURL.js": ` + browser.test.log("Navigating to XPI url from " + window.location.href); + const link = document.createElement("a"); + link.id = "xpi-link"; + link.href = "${xpiURL}"; + link.textContent = "Link to XPI file"; + document.body.appendChild(link); + link.click(); + `, + }, + }); + + await extension.startup(); + + await SpecialPowers.spawn(browser, [midFrameURL], async frameURL => { + const frame = content.document.createElement("iframe"); + frame.src = frameURL; + content.document.body.appendChild(frame); + }); + + await assertFn({ browser }); + + await extension.unload(); + await SpecialPowers.popPrefEnv(); + }); +} + +add_task(async function testInstallBlockedOnNavigationFromCrossOriginFrame() { + const promiseOriginBlocked = TestUtils.topicObserved( + "addon-install-origin-blocked" + ); + + await testSubframeInstallOnNavigation({ + topFrameURL: "https://test1.example.com/", + midFrameURL: "https://example.org/", + bottomFrameURL: "https://test1.example.com/installTrigger", + xpiURL: XPI_URL, + assertFn: async () => { + await promiseOriginBlocked; + Assert.deepEqual( + await AddonManager.getAllInstalls(), + [], + "Expects no pending addon install" + ); + }, + }); +}); + +add_task(async function testInstallPromptedOnNavigationFromSameOriginFrame() { + const promisePromptedInstallFromThirdParty = TestUtils.topicObserved( + "addon-install-blocked" + ); + + await testSubframeInstallOnNavigation({ + topFrameURL: "https://test2.example.com/", + midFrameURL: "https://test1.example.com/", + bottomFrameURL: "https://test2.example.com/installTrigger", + xpiURL: XPI_URL, + assertFn: async () => { + const [subject] = await promisePromptedInstallFromThirdParty; + let installInfo = subject.wrappedJSObject; + ok(installInfo, "Got a blocked addon install pending"); + installInfo.cancel(); + }, + }); +}); + +add_task(async function testInstallTriggerBlockedFromCrossOriginFrame() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.InstallTrigger.enabled", true], + ["extensions.InstallTriggerImpl.enabled", true], + // Relax the user input requirements while running this test. + ["xpinstall.userActivation.required", false], + ], + }); + + const promiseOriginBlocked = TestUtils.topicObserved( + "addon-install-origin-blocked" + ); + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["https://example.org/*"], + js: ["createFrame.js"], + all_frames: true, + }, + { + matches: ["https://test1.example.com/installTrigger*"], + js: ["installTrigger.js"], + all_frames: true, + }, + ], + }, + files: { + "createFrame.js": function () { + const frame = document.createElement("iframe"); + frame.src = "https://test1.example.com/installTrigger/"; + document.body.appendChild(frame); + }, + "installTrigger.js": ` + window.InstallTrigger.install({extension: "${XPI_URL}"}); + `, + }, + }); + + await extension.startup(); + + await BrowserTestUtils.withNewTab( + "https://test1.example.com", + async browser => { + await SpecialPowers.spawn(browser, [], async () => { + const frame = content.document.createElement("iframe"); + frame.src = "https://example.org"; + content.document.body.appendChild(frame); + }); + + await promiseOriginBlocked; + Assert.deepEqual( + await AddonManager.getAllInstalls(), + [], + "Expects no pending addon install" + ); + } + ); + + await extension.unload(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function testInstallTriggerPromptedFromSameOriginFrame() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.InstallTrigger.enabled", true], + ["extensions.InstallTriggerImpl.enabled", true], + // Relax the user input requirements while running this test. + ["xpinstall.userActivation.required", false], + ], + }); + + const promisePromptedInstallFromThirdParty = TestUtils.topicObserved( + "addon-install-blocked" + ); + + await BrowserTestUtils.withNewTab("https://example.com", async browser => { + await SpecialPowers.spawn(browser, [XPI_URL], async xpiURL => { + const frame = content.document.createElement("iframe"); + frame.src = "https://example.com"; + const frameLoaded = new Promise(resolve => { + frame.addEventListener("load", resolve, { once: true }); + }); + content.document.body.appendChild(frame); + await frameLoaded; + frame.contentWindow.InstallTrigger.install({ URL: xpiURL }); + }); + + const [subject] = await promisePromptedInstallFromThirdParty; + let installInfo = subject.wrappedJSObject; + ok(installInfo, "Got a blocked addon install pending"); + is( + installInfo?.installs?.[0]?.state, + Services.prefs.getBoolPref( + "extensions.postDownloadThirdPartyPrompt", + false + ) + ? AddonManager.STATE_DOWNLOADED + : AddonManager.STATE_AVAILABLE, + "Got a pending addon install" + ); + await installInfo.cancel(); + }); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_task_next_test.js b/toolkit/mozapps/extensions/test/browser/browser_task_next_test.js new file mode 100644 index 0000000000..f8e3293b82 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_task_next_test.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that we throw if a test created with add_task() +// calls run_next_test + +add_task(async function run_next_throws() { + let err = null; + try { + run_next_test(); + } catch (e) { + err = e; + info("run_next_test threw " + err); + } + ok(err, "run_next_test() should throw an error inside an add_task test"); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_updateid.js b/toolkit/mozapps/extensions/test/browser/browser_updateid.js new file mode 100644 index 0000000000..c6e6d3030f --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_updateid.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that updates that change an add-on's ID show up correctly in the UI + +var gProvider; +var gManagerWindow; +var gCategoryUtilities; + +function getName(item) { + return item.addonNameEl.textContent; +} + +async function getUpdateButton(item) { + let button = item.querySelector('[action="install-update"]'); + let panel = button.closest("panel-list"); + let shown = BrowserTestUtils.waitForEvent(panel, "shown"); + let moreOptionsButton = item.querySelector('[action="more-options"]'); + EventUtils.synthesizeMouseAtCenter(moreOptionsButton, {}, item.ownerGlobal); + await shown; + return button; +} + +add_task(async function test_updateid() { + // Close the existing about:addons tab and unrestier the existing MockProvider + // instance if a previous failed test has not been able to clear them. + if (gManagerWindow) { + await close_manager(gManagerWindow); + } + if (gProvider) { + gProvider.unregister(); + } + + gProvider = new MockProvider(); + + gProvider.createAddons([ + { + id: "addon1@tests.mozilla.org", + name: "manually updating addon", + version: "1.0", + applyBackgroundUpdates: AddonManager.AUTOUPDATE_DISABLE, + }, + ]); + + gManagerWindow = await open_manager("addons://list/extension"); + gCategoryUtilities = new CategoryUtilities(gManagerWindow); + await gCategoryUtilities.openType("extension"); + + gProvider.createInstalls([ + { + name: "updated add-on", + existingAddon: gProvider.addons[0], + version: "2.0", + }, + ]); + var newAddon = new MockAddon("addon2@tests.mozilla.org"); + newAddon.name = "updated add-on"; + newAddon.version = "2.0"; + newAddon.pendingOperations = AddonManager.PENDING_INSTALL; + gProvider.installs[0]._addonToInstall = newAddon; + + var item = getAddonCard(gManagerWindow, "addon1@tests.mozilla.org"); + is( + getName(item), + "manually updating addon", + "Should show the old name in the list" + ); + const { name, version } = await get_tooltip_info(item, gManagerWindow); + is( + name, + "manually updating addon", + "Should show the old name in the tooltip" + ); + is(version, "1.0", "Should still show the old version in the tooltip"); + + var update = await getUpdateButton(item); + is_element_visible(update, "Update button should be visible"); + + item = getAddonCard(gManagerWindow, "addon2@tests.mozilla.org"); + is(item, null, "Should not show the new version in the list"); + + await close_manager(gManagerWindow); + gManagerWindow = null; + gProvider.unregister(); + gProvider = null; +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_updatessl.js b/toolkit/mozapps/extensions/test/browser/browser_updatessl.js new file mode 100644 index 0000000000..9dbeec4a84 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_updatessl.js @@ -0,0 +1,389 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +let { AddonUpdateChecker } = ChromeUtils.importESModule( + "resource://gre/modules/addons/AddonUpdateChecker.sys.mjs" +); + +const updatejson = RELATIVE_DIR + "browser_updatessl.json"; +const redirect = RELATIVE_DIR + "redirect.sjs?"; +const SUCCESS = 0; +const DOWNLOAD_ERROR = AddonManager.ERROR_DOWNLOAD_ERROR; + +const HTTP = "http://example.com/"; +const HTTPS = "https://example.com/"; +const NOCERT = "https://nocert.example.com/"; +const SELFSIGNED = "https://self-signed.example.com/"; +const UNTRUSTED = "https://untrusted.example.com/"; +const EXPIRED = "https://expired.example.com/"; + +const PREF_UPDATE_REQUIREBUILTINCERTS = "extensions.update.requireBuiltInCerts"; + +var gTests = []; +var gStart = 0; +var gLast = 0; + +var HTTPObserver = { + observeActivity( + aChannel, + aType, + aSubtype, + aTimestamp, + aSizeData, + aStringData + ) { + aChannel.QueryInterface(Ci.nsIChannel); + + dump( + "*** HTTP Activity 0x" + + aType.toString(16) + + " 0x" + + aSubtype.toString(16) + + " " + + aChannel.URI.spec + + "\n" + ); + }, +}; + +function test() { + gStart = Date.now(); + requestLongerTimeout(4); + waitForExplicitFinish(); + + let observerService = Cc[ + "@mozilla.org/network/http-activity-distributor;1" + ].getService(Ci.nsIHttpActivityDistributor); + observerService.addObserver(HTTPObserver); + + registerCleanupFunction(function () { + observerService.removeObserver(HTTPObserver); + }); + + run_next_test(); +} + +function end_test() { + var cos = Cc["@mozilla.org/security/certoverride;1"].getService( + Ci.nsICertOverrideService + ); + cos.clearValidityOverride("nocert.example.com", -1, {}); + cos.clearValidityOverride("self-signed.example.com", -1, {}); + cos.clearValidityOverride("untrusted.example.com", -1, {}); + cos.clearValidityOverride("expired.example.com", -1, {}); + + info("All tests completed in " + (Date.now() - gStart) + "ms"); + finish(); +} + +function add_update_test(mainURL, redirectURL, expectedStatus) { + gTests.push([mainURL, redirectURL, expectedStatus]); +} + +function run_update_tests(callback) { + function run_next_update_test() { + if (!gTests.length) { + callback(); + return; + } + gLast = Date.now(); + + let [mainURL, redirectURL, expectedStatus] = gTests.shift(); + if (redirectURL) { + var url = mainURL + redirect + redirectURL + updatejson; + var message = + "Should have seen the right result for an update check redirected from " + + mainURL + + " to " + + redirectURL; + } else { + url = mainURL + updatejson; + message = + "Should have seen the right result for an update check from " + mainURL; + } + + AddonUpdateChecker.checkForUpdates("addon1@tests.mozilla.org", url, { + onUpdateCheckComplete(updates) { + is(updates.length, 1, "Should be the right number of results"); + is(SUCCESS, expectedStatus, message); + info("Update test ran in " + (Date.now() - gLast) + "ms"); + run_next_update_test(); + }, + + onUpdateCheckError(status) { + is(status, expectedStatus, message); + info("Update test ran in " + (Date.now() - gLast) + "ms"); + run_next_update_test(); + }, + }); + } + + run_next_update_test(); +} + +// Runs tests with built-in certificates required and no certificate exceptions. +add_test(async function test_builtin_required() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_UPDATE_REQUIREBUILTINCERTS, true]], + }); + // Tests that a simple update.json retrieval works as expected. + add_update_test(HTTP, null, SUCCESS); + add_update_test(HTTPS, null, DOWNLOAD_ERROR); + add_update_test(NOCERT, null, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, null, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, null, DOWNLOAD_ERROR); + add_update_test(EXPIRED, null, DOWNLOAD_ERROR); + + // Tests that redirecting from http to other servers works as expected + add_update_test(HTTP, HTTP, SUCCESS); + add_update_test(HTTP, HTTPS, SUCCESS); + add_update_test(HTTP, NOCERT, DOWNLOAD_ERROR); + add_update_test(HTTP, SELFSIGNED, DOWNLOAD_ERROR); + add_update_test(HTTP, UNTRUSTED, DOWNLOAD_ERROR); + add_update_test(HTTP, EXPIRED, DOWNLOAD_ERROR); + + // Tests that redirecting from valid https to other servers works as expected + add_update_test(HTTPS, HTTP, DOWNLOAD_ERROR); + add_update_test(HTTPS, HTTPS, DOWNLOAD_ERROR); + add_update_test(HTTPS, NOCERT, DOWNLOAD_ERROR); + add_update_test(HTTPS, SELFSIGNED, DOWNLOAD_ERROR); + add_update_test(HTTPS, UNTRUSTED, DOWNLOAD_ERROR); + add_update_test(HTTPS, EXPIRED, DOWNLOAD_ERROR); + + // Tests that redirecting from nocert https to other servers works as expected + add_update_test(NOCERT, HTTP, DOWNLOAD_ERROR); + add_update_test(NOCERT, HTTPS, DOWNLOAD_ERROR); + add_update_test(NOCERT, NOCERT, DOWNLOAD_ERROR); + add_update_test(NOCERT, SELFSIGNED, DOWNLOAD_ERROR); + add_update_test(NOCERT, UNTRUSTED, DOWNLOAD_ERROR); + add_update_test(NOCERT, EXPIRED, DOWNLOAD_ERROR); + + // Tests that redirecting from self-signed https to other servers works as expected + add_update_test(SELFSIGNED, HTTP, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, HTTPS, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, NOCERT, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, SELFSIGNED, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, UNTRUSTED, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, EXPIRED, DOWNLOAD_ERROR); + + // Tests that redirecting from untrusted https to other servers works as expected + add_update_test(UNTRUSTED, HTTP, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, HTTPS, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, NOCERT, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, SELFSIGNED, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, UNTRUSTED, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, EXPIRED, DOWNLOAD_ERROR); + + // Tests that redirecting from expired https to other servers works as expected + add_update_test(EXPIRED, HTTP, DOWNLOAD_ERROR); + add_update_test(EXPIRED, HTTPS, DOWNLOAD_ERROR); + add_update_test(EXPIRED, NOCERT, DOWNLOAD_ERROR); + add_update_test(EXPIRED, SELFSIGNED, DOWNLOAD_ERROR); + add_update_test(EXPIRED, UNTRUSTED, DOWNLOAD_ERROR); + add_update_test(EXPIRED, EXPIRED, DOWNLOAD_ERROR); + + run_update_tests(run_next_test); +}); + +// Runs tests without requiring built-in certificates and no certificate +// exceptions. +add_test(async function test_builtin_not_required() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_UPDATE_REQUIREBUILTINCERTS, false]], + }); + + // Tests that a simple update.json retrieval works as expected. + add_update_test(HTTP, null, SUCCESS); + add_update_test(HTTPS, null, SUCCESS); + add_update_test(NOCERT, null, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, null, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, null, DOWNLOAD_ERROR); + add_update_test(EXPIRED, null, DOWNLOAD_ERROR); + + // Tests that redirecting from http to other servers works as expected + add_update_test(HTTP, HTTP, SUCCESS); + add_update_test(HTTP, HTTPS, SUCCESS); + add_update_test(HTTP, NOCERT, DOWNLOAD_ERROR); + add_update_test(HTTP, SELFSIGNED, DOWNLOAD_ERROR); + add_update_test(HTTP, UNTRUSTED, DOWNLOAD_ERROR); + add_update_test(HTTP, EXPIRED, DOWNLOAD_ERROR); + + // Tests that redirecting from valid https to other servers works as expected + add_update_test(HTTPS, HTTP, DOWNLOAD_ERROR); + add_update_test(HTTPS, HTTPS, SUCCESS); + add_update_test(HTTPS, NOCERT, DOWNLOAD_ERROR); + add_update_test(HTTPS, SELFSIGNED, DOWNLOAD_ERROR); + add_update_test(HTTPS, UNTRUSTED, DOWNLOAD_ERROR); + add_update_test(HTTPS, EXPIRED, DOWNLOAD_ERROR); + + // Tests that redirecting from nocert https to other servers works as expected + add_update_test(NOCERT, HTTP, DOWNLOAD_ERROR); + add_update_test(NOCERT, HTTPS, DOWNLOAD_ERROR); + add_update_test(NOCERT, NOCERT, DOWNLOAD_ERROR); + add_update_test(NOCERT, SELFSIGNED, DOWNLOAD_ERROR); + add_update_test(NOCERT, UNTRUSTED, DOWNLOAD_ERROR); + add_update_test(NOCERT, EXPIRED, DOWNLOAD_ERROR); + + // Tests that redirecting from self-signed https to other servers works as expected + add_update_test(SELFSIGNED, HTTP, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, HTTPS, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, NOCERT, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, SELFSIGNED, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, UNTRUSTED, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, EXPIRED, DOWNLOAD_ERROR); + + // Tests that redirecting from untrusted https to other servers works as expected + add_update_test(UNTRUSTED, HTTP, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, HTTPS, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, NOCERT, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, SELFSIGNED, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, UNTRUSTED, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, EXPIRED, DOWNLOAD_ERROR); + + // Tests that redirecting from expired https to other servers works as expected + add_update_test(EXPIRED, HTTP, DOWNLOAD_ERROR); + add_update_test(EXPIRED, HTTPS, DOWNLOAD_ERROR); + add_update_test(EXPIRED, NOCERT, DOWNLOAD_ERROR); + add_update_test(EXPIRED, SELFSIGNED, DOWNLOAD_ERROR); + add_update_test(EXPIRED, UNTRUSTED, DOWNLOAD_ERROR); + add_update_test(EXPIRED, EXPIRED, DOWNLOAD_ERROR); + + run_update_tests(run_next_test); +}); + +// Set up overrides for the next test. +add_test(() => { + addCertOverrides().then(run_next_test); +}); + +// Runs tests with built-in certificates required and all certificate exceptions. +add_test(async function test_builtin_required_overrides() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_UPDATE_REQUIREBUILTINCERTS, true]], + }); + + // Tests that a simple update.json retrieval works as expected. + add_update_test(HTTP, null, SUCCESS); + add_update_test(HTTPS, null, DOWNLOAD_ERROR); + add_update_test(NOCERT, null, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, null, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, null, DOWNLOAD_ERROR); + add_update_test(EXPIRED, null, DOWNLOAD_ERROR); + + // Tests that redirecting from http to other servers works as expected + add_update_test(HTTP, HTTP, SUCCESS); + add_update_test(HTTP, HTTPS, SUCCESS); + add_update_test(HTTP, NOCERT, SUCCESS); + add_update_test(HTTP, SELFSIGNED, SUCCESS); + add_update_test(HTTP, UNTRUSTED, SUCCESS); + add_update_test(HTTP, EXPIRED, SUCCESS); + + // Tests that redirecting from valid https to other servers works as expected + add_update_test(HTTPS, HTTP, DOWNLOAD_ERROR); + add_update_test(HTTPS, HTTPS, DOWNLOAD_ERROR); + add_update_test(HTTPS, NOCERT, DOWNLOAD_ERROR); + add_update_test(HTTPS, SELFSIGNED, DOWNLOAD_ERROR); + add_update_test(HTTPS, UNTRUSTED, DOWNLOAD_ERROR); + add_update_test(HTTPS, EXPIRED, DOWNLOAD_ERROR); + + // Tests that redirecting from nocert https to other servers works as expected + add_update_test(NOCERT, HTTP, DOWNLOAD_ERROR); + add_update_test(NOCERT, HTTPS, DOWNLOAD_ERROR); + add_update_test(NOCERT, NOCERT, DOWNLOAD_ERROR); + add_update_test(NOCERT, SELFSIGNED, DOWNLOAD_ERROR); + add_update_test(NOCERT, UNTRUSTED, DOWNLOAD_ERROR); + add_update_test(NOCERT, EXPIRED, DOWNLOAD_ERROR); + + // Tests that redirecting from self-signed https to other servers works as expected + add_update_test(SELFSIGNED, HTTP, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, HTTPS, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, NOCERT, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, SELFSIGNED, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, UNTRUSTED, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, EXPIRED, DOWNLOAD_ERROR); + + // Tests that redirecting from untrusted https to other servers works as expected + add_update_test(UNTRUSTED, HTTP, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, HTTPS, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, NOCERT, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, SELFSIGNED, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, UNTRUSTED, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, EXPIRED, DOWNLOAD_ERROR); + + // Tests that redirecting from expired https to other servers works as expected + add_update_test(EXPIRED, HTTP, DOWNLOAD_ERROR); + add_update_test(EXPIRED, HTTPS, DOWNLOAD_ERROR); + add_update_test(EXPIRED, NOCERT, DOWNLOAD_ERROR); + add_update_test(EXPIRED, SELFSIGNED, DOWNLOAD_ERROR); + add_update_test(EXPIRED, UNTRUSTED, DOWNLOAD_ERROR); + add_update_test(EXPIRED, EXPIRED, DOWNLOAD_ERROR); + + run_update_tests(run_next_test); +}); + +// Runs tests without requiring built-in certificates and all certificate +// exceptions. +add_test(async function test_builtin_not_required_overrides() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_UPDATE_REQUIREBUILTINCERTS, false]], + }); + + // Tests that a simple update.json retrieval works as expected. + add_update_test(HTTP, null, SUCCESS); + add_update_test(HTTPS, null, SUCCESS); + add_update_test(NOCERT, null, SUCCESS); + add_update_test(SELFSIGNED, null, SUCCESS); + add_update_test(UNTRUSTED, null, SUCCESS); + add_update_test(EXPIRED, null, SUCCESS); + + // Tests that redirecting from http to other servers works as expected + add_update_test(HTTP, HTTP, SUCCESS); + add_update_test(HTTP, HTTPS, SUCCESS); + add_update_test(HTTP, NOCERT, SUCCESS); + add_update_test(HTTP, SELFSIGNED, SUCCESS); + add_update_test(HTTP, UNTRUSTED, SUCCESS); + add_update_test(HTTP, EXPIRED, SUCCESS); + + // Tests that redirecting from valid https to other servers works as expected + add_update_test(HTTPS, HTTP, DOWNLOAD_ERROR); + add_update_test(HTTPS, HTTPS, SUCCESS); + add_update_test(HTTPS, NOCERT, SUCCESS); + add_update_test(HTTPS, SELFSIGNED, SUCCESS); + add_update_test(HTTPS, UNTRUSTED, SUCCESS); + add_update_test(HTTPS, EXPIRED, SUCCESS); + + // Tests that redirecting from nocert https to other servers works as expected + add_update_test(NOCERT, HTTP, DOWNLOAD_ERROR); + add_update_test(NOCERT, HTTPS, SUCCESS); + add_update_test(NOCERT, NOCERT, SUCCESS); + add_update_test(NOCERT, SELFSIGNED, SUCCESS); + add_update_test(NOCERT, UNTRUSTED, SUCCESS); + add_update_test(NOCERT, EXPIRED, SUCCESS); + + // Tests that redirecting from self-signed https to other servers works as expected + add_update_test(SELFSIGNED, HTTP, DOWNLOAD_ERROR); + add_update_test(SELFSIGNED, HTTPS, SUCCESS); + add_update_test(SELFSIGNED, NOCERT, SUCCESS); + add_update_test(SELFSIGNED, SELFSIGNED, SUCCESS); + add_update_test(SELFSIGNED, UNTRUSTED, SUCCESS); + add_update_test(SELFSIGNED, EXPIRED, SUCCESS); + + // Tests that redirecting from untrusted https to other servers works as expected + add_update_test(UNTRUSTED, HTTP, DOWNLOAD_ERROR); + add_update_test(UNTRUSTED, HTTPS, SUCCESS); + add_update_test(UNTRUSTED, NOCERT, SUCCESS); + add_update_test(UNTRUSTED, SELFSIGNED, SUCCESS); + add_update_test(UNTRUSTED, UNTRUSTED, SUCCESS); + add_update_test(UNTRUSTED, EXPIRED, SUCCESS); + + // Tests that redirecting from expired https to other servers works as expected + add_update_test(EXPIRED, HTTP, DOWNLOAD_ERROR); + add_update_test(EXPIRED, HTTPS, SUCCESS); + add_update_test(EXPIRED, NOCERT, SUCCESS); + add_update_test(EXPIRED, SELFSIGNED, SUCCESS); + add_update_test(EXPIRED, UNTRUSTED, SUCCESS); + add_update_test(EXPIRED, EXPIRED, SUCCESS); + + run_update_tests(run_next_test); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_updatessl.json b/toolkit/mozapps/extensions/test/browser/browser_updatessl.json new file mode 100644 index 0000000000..223d1ef2d3 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_updatessl.json @@ -0,0 +1,17 @@ +{ + "addons": { + "addon1@tests.mozilla.org": { + "updates": [ + { + "applications": { + "gecko": { + "strict_min_version": "0", + "advisory_max_version": "20" + } + }, + "version": "2.0" + } + ] + } + } +} diff --git a/toolkit/mozapps/extensions/test/browser/browser_updatessl.json^headers^ b/toolkit/mozapps/extensions/test/browser/browser_updatessl.json^headers^ new file mode 100644 index 0000000000..2e4f8163bb --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_updatessl.json^headers^ @@ -0,0 +1 @@ +Connection: close diff --git a/toolkit/mozapps/extensions/test/browser/browser_verify_l10n_strings.js b/toolkit/mozapps/extensions/test/browser/browser_verify_l10n_strings.js new file mode 100644 index 0000000000..e245e3a6e4 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_verify_l10n_strings.js @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +ChromeUtils.defineESModuleGetters(this, { + BuiltInThemes: "resource:///modules/BuiltInThemes.sys.mjs", +}); + +// Maps add-on descriptors to updated Fluent IDs. Keep it in sync +// with the list in XPIDatabase.sys.mjs. +const updatedAddonFluentIds = new Map([ + ["extension-default-theme-name", "extension-default-theme-name-auto"], +]); + +add_task(async function test_ensure_bundled_addons_are_localized() { + let l10nReg = L10nRegistry.getInstance(); + let bundles = l10nReg.generateBundlesSync( + ["en-US"], + ["browser/appExtensionFields.ftl"] + ); + let addons = await AddonManager.getAllAddons(); + let standardBuiltInThemes = addons.filter( + addon => + addon.isBuiltin && + addon.type === "theme" && + !addon.id.endsWith("colorway@mozilla.org") + ); + let bundle = bundles.next().value; + + ok(!!standardBuiltInThemes.length, "Standard built-in themes should exist"); + + for (let standardTheme of standardBuiltInThemes) { + let l10nId = standardTheme.id.replace("@mozilla.org", ""); + for (let prop of ["name", "description"]) { + let defaultFluentId = `extension-${l10nId}-${prop}`; + let fluentId = + updatedAddonFluentIds.get(defaultFluentId) || defaultFluentId; + ok( + bundle.hasMessage(fluentId), + `l10n id for ${standardTheme.id} \"${prop}\" attribute should exist` + ); + } + } + + let colorwayThemes = Array.from(BuiltInThemes.builtInThemeMap.keys()).filter( + id => id.endsWith("colorway@mozilla.org") + ); + ok(!!colorwayThemes.length, "Colorway themes should exist"); + for (let id of colorwayThemes) { + let l10nId = id.replace("@mozilla.org", ""); + let [, variantName] = l10nId.split("-", 2); + if (variantName != "colorway") { + let defaultFluentId = `extension-colorways-${variantName}-name`; + let fluentId = + updatedAddonFluentIds.get(defaultFluentId) || defaultFluentId; + ok( + bundle.hasMessage(fluentId), + `l10n id for ${id} \"name\" attribute should exist` + ); + } + } +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi.js b/toolkit/mozapps/extensions/test/browser/browser_webapi.js new file mode 100644 index 0000000000..853cd3902a --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_webapi.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.webapi.testing", true]], + }); +}); + +function testWithAPI(task) { + return async function () { + await BrowserTestUtils.withNewTab(TESTPAGE, task); + }; +} + +let gProvider = new MockProvider(); + +let addons = gProvider.createAddons([ + { + id: "addon1@tests.mozilla.org", + name: "Test add-on 1", + version: "2.1", + description: "Short description", + type: "extension", + userDisabled: false, + isActive: true, + }, + { + id: "addon2@tests.mozilla.org", + name: "Test add-on 2", + version: "5.3.7ab", + description: null, + type: "theme", + userDisabled: false, + isActive: false, + }, + { + id: "addon3@tests.mozilla.org", + name: "Test add-on 3", + version: "1", + description: "Longer description", + type: "extension", + userDisabled: true, + isActive: false, + }, + { + id: "addon4@tests.mozilla.org", + name: "Test add-on 4", + version: "1", + description: "Longer description", + type: "extension", + userDisabled: false, + isActive: true, + }, +]); + +addons[3].permissions &= ~AddonManager.PERM_CAN_UNINSTALL; + +function API_getAddonByID(browser, id) { + return SpecialPowers.spawn(browser, [id], async function (id) { + let addon = await content.navigator.mozAddonManager.getAddonByID(id); + let addonDetails = {}; + for (let prop in addon) { + addonDetails[prop] = addon[prop]; + } + // We can't send native objects back so clone its properties. + return JSON.parse(JSON.stringify(addonDetails)); + }); +} + +add_task( + testWithAPI(async function (browser) { + function compareObjects(web, real) { + ok( + !!Object.keys(web).length, + "Got a valid mozAddonManager addon object dump" + ); + + for (let prop of Object.keys(web)) { + let webVal = web[prop]; + let realVal = real[prop]; + + switch (prop) { + case "isEnabled": + realVal = !real.userDisabled; + break; + + case "canUninstall": + realVal = Boolean( + real.permissions & AddonManager.PERM_CAN_UNINSTALL + ); + break; + } + + // null and undefined don't compare well so stringify them first + if (realVal === null || realVal === undefined) { + realVal = `${realVal}`; + webVal = `${webVal}`; + } + + is( + webVal, + realVal, + `Property ${prop} should have the right value in add-on ${real.id}` + ); + } + } + + let [a1, a2, a3] = await promiseAddonsByIDs([ + "addon1@tests.mozilla.org", + "addon2@tests.mozilla.org", + "addon3@tests.mozilla.org", + ]); + let w1 = await API_getAddonByID(browser, "addon1@tests.mozilla.org"); + let w2 = await API_getAddonByID(browser, "addon2@tests.mozilla.org"); + let w3 = await API_getAddonByID(browser, "addon3@tests.mozilla.org"); + + compareObjects(w1, a1); + compareObjects(w2, a2); + compareObjects(w3, a3); + }) +); diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_abuse_report.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_abuse_report.js new file mode 100644 index 0000000000..b9ea0f6a93 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_abuse_report.js @@ -0,0 +1,375 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* eslint max-len: ["error", 80] */ + +loadTestSubscript("head_abuse_report.js"); + +const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`; +const TELEMETRY_EVENTS_FILTERS = { + category: "addonsManager", + method: "report", +}; +const REPORT_PROP_NAMES = [ + "addon", + "addon_signature", + "reason", + "message", + "report_entry_point", +]; + +function getObjectProps(obj, propNames) { + const res = {}; + for (const k of propNames) { + res[k] = obj[k]; + } + return res; +} + +async function assertSubmittedReport(expectedReportProps) { + let reportSubmitted; + const onReportSubmitted = AbuseReportTestUtils.promiseReportSubmitHandled( + ({ data, request, response }) => { + reportSubmitted = JSON.parse(data); + handleSubmitRequest({ request, response }); + } + ); + + let panelEl = await AbuseReportTestUtils.promiseReportDialogRendered(); + + let promiseWinClosed = waitClosedWindow(); + let promisePanelUpdated = AbuseReportTestUtils.promiseReportUpdated( + panelEl, + "submit" + ); + panelEl._form.elements.reason.value = expectedReportProps.reason; + AbuseReportTestUtils.clickPanelButton(panelEl._btnNext); + await promisePanelUpdated; + + panelEl._form.elements.message.value = expectedReportProps.message; + // Reset the timestamp of the last report between tests. + AbuseReporter._lastReportTimestamp = null; + AbuseReportTestUtils.clickPanelButton(panelEl._btnSubmit); + await Promise.all([onReportSubmitted, promiseWinClosed]); + + ok(!panelEl.ownerGlobal, "Report dialog window is closed"); + Assert.deepEqual( + getObjectProps(reportSubmitted, REPORT_PROP_NAMES), + expectedReportProps, + "Got the expected report data submitted" + ); +} + +add_setup(async function () { + await AbuseReportTestUtils.setup(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.webapi.testing", true], + ["extensions.abuseReport.amWebAPI.enabled", true], + // Make sure the integrated abuse report panel is the one enabled + // while this test file runs (instead of the AMO hosted form). + // NOTE: behaviors expected when amoFormEnabled is true are tested + // in the separate browser_amo_abuse_report.js test file. + ["extensions.abuseReport.amoFormEnabled", false], + ], + }); +}); + +add_task(async function test_report_installed_addon_cancelled() { + Services.telemetry.clearEvents(); + + await BrowserTestUtils.withNewTab(TESTPAGE, async browser => { + const extension = await installTestExtension(ADDON_ID); + + let reportEnabled = await SpecialPowers.spawn(browser, [], () => { + return content.navigator.mozAddonManager.abuseReportPanelEnabled; + }); + + is(reportEnabled, true, "Expect abuseReportPanelEnabled to be true"); + + info("Test reportAbuse result on user cancelled report"); + + let promiseNewWindow = waitForNewWindow(); + let promiseWebAPIResult = SpecialPowers.spawn( + browser, + [ADDON_ID], + addonId => content.navigator.mozAddonManager.reportAbuse(addonId) + ); + + let win = await promiseNewWindow; + is(win, AbuseReportTestUtils.getReportDialog(), "Got the report dialog"); + + let panelEl = await AbuseReportTestUtils.promiseReportDialogRendered(); + + let promiseWinClosed = waitClosedWindow(); + AbuseReportTestUtils.clickPanelButton(panelEl._btnCancel); + let reportResult = await promiseWebAPIResult; + is( + reportResult, + false, + "Expect reportAbuse to resolve to false on user cancelled report" + ); + await promiseWinClosed; + ok(!panelEl.ownerGlobal, "Report dialog window is closed"); + + await extension.unload(); + }); + + // Expect no telemetry events collected for user cancelled reports. + TelemetryTestUtils.assertEvents([], TELEMETRY_EVENTS_FILTERS); +}); + +add_task(async function test_report_installed_addon_submitted() { + Services.telemetry.clearEvents(); + + await BrowserTestUtils.withNewTab(TESTPAGE, async browser => { + const extension = await installTestExtension(ADDON_ID); + + let promiseNewWindow = waitForNewWindow(); + let promiseWebAPIResult = SpecialPowers.spawn(browser, [ADDON_ID], id => + content.navigator.mozAddonManager.reportAbuse(id) + ); + let win = await promiseNewWindow; + is(win, AbuseReportTestUtils.getReportDialog(), "Got the report dialog"); + + await assertSubmittedReport({ + addon: ADDON_ID, + addon_signature: "missing", + message: "fake report message", + reason: "unwanted", + report_entry_point: "amo", + }); + + let reportResult = await promiseWebAPIResult; + is( + reportResult, + true, + "Expect reportAbuse to resolve to false on user cancelled report" + ); + + await extension.unload(); + }); + + TelemetryTestUtils.assertEvents( + [ + { + object: "amo", + value: ADDON_ID, + extra: { addon_type: "extension" }, + }, + ], + TELEMETRY_EVENTS_FILTERS + ); +}); + +add_task(async function test_report_unknown_not_installed_addon() { + const addonId = "unknown-addon@mochi.test"; + Services.telemetry.clearEvents(); + + await BrowserTestUtils.withNewTab(TESTPAGE, async browser => { + let promiseWebAPIResult = SpecialPowers.spawn(browser, [addonId], id => + content.navigator.mozAddonManager.reportAbuse(id).catch(err => { + return { name: err.name, message: err.message }; + }) + ); + + await Assert.deepEqual( + await promiseWebAPIResult, + { name: "Error", message: "Error creating abuse report" }, + "Got the expected rejected error on reporting unknown addon" + ); + + ok(!AbuseReportTestUtils.getReportDialog(), "No report dialog is open"); + }); + + TelemetryTestUtils.assertEvents( + [ + { + object: "amo", + value: addonId, + extra: { error_type: "ERROR_AMODETAILS_NOTFOUND" }, + }, + { + object: "amo", + value: addonId, + extra: { error_type: "ERROR_ADDON_NOTFOUND" }, + }, + ], + TELEMETRY_EVENTS_FILTERS + ); +}); + +add_task(async function test_report_not_installed_addon() { + const addonId = "not-installed-addon@mochi.test"; + Services.telemetry.clearEvents(); + + await BrowserTestUtils.withNewTab(TESTPAGE, async browser => { + const fakeAMODetails = { + name: "fake name", + current_version: { version: "1.0" }, + type: "extension", + icon_url: "http://test.addons.org/asserts/fake-icon-url.png", + homepage: "http://fake.url/homepage", + authors: [{ name: "author1", url: "http://fake.url/author1" }], + is_recommended: false, + }; + + AbuseReportTestUtils.amoAddonDetailsMap.set(addonId, fakeAMODetails); + registerCleanupFunction(() => + AbuseReportTestUtils.amoAddonDetailsMap.clear() + ); + + let promiseNewWindow = waitForNewWindow(); + + let promiseWebAPIResult = SpecialPowers.spawn(browser, [addonId], id => + content.navigator.mozAddonManager.reportAbuse(id) + ); + let win = await promiseNewWindow; + is(win, AbuseReportTestUtils.getReportDialog(), "Got the report dialog"); + + await assertSubmittedReport({ + addon: addonId, + addon_signature: "unknown", + message: "fake report message", + reason: "other", + report_entry_point: "amo", + }); + + let reportResult = await promiseWebAPIResult; + is( + reportResult, + true, + "Expect reportAbuse to resolve to true on submitted report" + ); + }); + + TelemetryTestUtils.assertEvents( + [ + { + object: "amo", + value: addonId, + extra: { addon_type: "extension" }, + }, + ], + TELEMETRY_EVENTS_FILTERS + ); +}); + +add_task(async function test_amo_report_on_report_already_inprogress() { + const extension = await installTestExtension(ADDON_ID); + const reportDialog = await AbuseReporter.openDialog( + ADDON_ID, + "menu", + gBrowser.selectedBrowser + ); + await AbuseReportTestUtils.promiseReportDialogRendered(); + ok(reportDialog.window, "Got an open report dialog"); + + let promiseWinClosed = waitClosedWindow(); + + await BrowserTestUtils.withNewTab(TESTPAGE, async browser => { + const promiseAMOResult = SpecialPowers.spawn(browser, [ADDON_ID], id => + content.navigator.mozAddonManager.reportAbuse(id) + ); + + await promiseWinClosed; + ok(reportDialog.window.closed, "previous report dialog should be closed"); + + is( + await reportDialog.promiseAMOResult, + undefined, + "old report cancelled after AMO called mozAddonManager.reportAbuse" + ); + + const panelEl = await AbuseReportTestUtils.promiseReportDialogRendered(); + + const { report } = AbuseReportTestUtils.getReportDialogParams(); + Assert.deepEqual( + { + reportEntryPoint: report.reportEntryPoint, + addonId: report.addon.id, + }, + { + reportEntryPoint: "amo", + addonId: ADDON_ID, + }, + "Got the expected report from the opened report dialog" + ); + + promiseWinClosed = waitClosedWindow(); + AbuseReportTestUtils.clickPanelButton(panelEl._btnCancel); + await promiseWinClosed; + + is( + await promiseAMOResult, + false, + "AMO report request resolved to false on cancel button clicked" + ); + }); + + await extension.unload(); +}); + +add_task(async function test_reject_on_unsupported_addon_types() { + const addonId = "not-supported-addon-type@mochi.test"; + + await BrowserTestUtils.withNewTab(TESTPAGE, async browser => { + const fakeAMODetails = { + name: "fake name", + current_version: { version: "1.0" }, + type: "fake-unsupported-addon-type", + }; + + AbuseReportTestUtils.amoAddonDetailsMap.set(addonId, fakeAMODetails); + registerCleanupFunction(() => + AbuseReportTestUtils.amoAddonDetailsMap.clear() + ); + + let webAPIResult = await SpecialPowers.spawn(browser, [addonId], id => + content.navigator.mozAddonManager.reportAbuse(id).then( + res => ({ gotRejection: false, result: res }), + err => ({ gotRejection: true, message: err.message }) + ) + ); + + Assert.deepEqual( + webAPIResult, + { gotRejection: true, message: "Error creating abuse report" }, + "Got the expected rejection from mozAddonManager.reportAbuse" + ); + }); +}); + +add_task(async function test_report_on_disabled_webapi() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.abuseReport.amWebAPI.enabled", false]], + }); + + await BrowserTestUtils.withNewTab(TESTPAGE, async browser => { + let reportEnabled = await SpecialPowers.spawn(browser, [], () => { + return content.navigator.mozAddonManager.abuseReportPanelEnabled; + }); + + is(reportEnabled, false, "Expect abuseReportPanelEnabled to be false"); + + info("Test reportAbuse result on report webAPI disabled"); + + let promiseWebAPIResult = SpecialPowers.spawn( + browser, + ["an-addon@mochi.test"], + addonId => + content.navigator.mozAddonManager.reportAbuse(addonId).catch(err => { + return { name: err.name, message: err.message }; + }) + ); + + Assert.deepEqual( + await promiseWebAPIResult, + { name: "Error", message: "amWebAPI reportAbuse not supported" }, + "Got the expected rejected error" + ); + }); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_access.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_access.js new file mode 100644 index 0000000000..aec6ddedca --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_access.js @@ -0,0 +1,146 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function check_frame_availability(browser) { + return check_availability(browser.browsingContext.children[0]); +} + +function check_availability(browser) { + return SpecialPowers.spawn(browser, [], async function () { + return content.document.getElementById("result").textContent == "true"; + }); +} + +// Test that initially the API isn't available in the test domain +add_task(async function test_not_available() { + await BrowserTestUtils.withNewTab( + `${SECURE_TESTROOT}webapi_checkavailable.html`, + async function test_not_available(browser) { + let available = await check_availability(browser); + ok(!available, "API should not be available."); + } + ); +}); + +// Test that with testing on the API is available in the test domain +add_task(async function test_available() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.webapi.testing", true]], + }); + + await BrowserTestUtils.withNewTab( + `${SECURE_TESTROOT}webapi_checkavailable.html`, + async function test_not_available(browser) { + let available = await check_availability(browser); + ok(available, "API should be available."); + } + ); +}); + +// Test that the API is not available in a bad domain +add_task(async function test_bad_domain() { + await BrowserTestUtils.withNewTab( + `${SECURE_TESTROOT2}webapi_checkavailable.html`, + async function test_not_available(browser) { + let available = await check_availability(browser); + ok(!available, "API should not be available."); + } + ); +}); + +// Test that the API is only available in https sites +add_task(async function test_not_available_http() { + await BrowserTestUtils.withNewTab( + `${TESTROOT}webapi_checkavailable.html`, + async function test_not_available(browser) { + let available = await check_availability(browser); + ok(!available, "API should not be available."); + } + ); +}); + +// Test that the API is available when in a frame of the test domain +add_task(async function test_available_framed() { + await BrowserTestUtils.withNewTab( + `${SECURE_TESTROOT}webapi_checkframed.html`, + async function test_available(browser) { + let available = await check_frame_availability(browser); + ok(available, "API should be available."); + } + ); +}); + +// Test that if the external frame is http then the inner frame doesn't have +// the API +add_task(async function test_not_available_http_framed() { + await BrowserTestUtils.withNewTab( + `${TESTROOT}webapi_checkframed.html`, + async function test_not_available(browser) { + let available = await check_frame_availability(browser); + ok(!available, "API should not be available."); + } + ); +}); + +// Test that if the external frame is a bad domain then the inner frame doesn't +// have the API +add_task(async function test_not_available_framed() { + await BrowserTestUtils.withNewTab( + `${SECURE_TESTROOT2}webapi_checkframed.html`, + async function test_not_available(browser) { + let available = await check_frame_availability(browser); + ok(!available, "API should not be available."); + } + ); +}); + +// Test that a window navigated to a bad domain doesn't allow access to the API +add_task(async function test_navigated_window() { + await BrowserTestUtils.withNewTab( + `${SECURE_TESTROOT2}webapi_checknavigatedwindow.html`, + async function test_available(browser) { + let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser); + + await SpecialPowers.spawn(browser, [], async function () { + await content.wrappedJSObject.openWindow(); + }); + + // Should be a new tab open + let tab = await tabPromise; + let loadPromise = BrowserTestUtils.browserLoaded( + gBrowser.getBrowserForTab(tab) + ); + + SpecialPowers.spawn(browser, [], async function () { + content.wrappedJSObject.navigate(); + }); + + await loadPromise; + + let available = await SpecialPowers.spawn(browser, [], async function () { + return content.wrappedJSObject.check(); + }); + + ok(!available, "API should not be available."); + + gBrowser.removeTab(tab); + } + ); +}); + +// Check that if a page is embedded in a chrome content UI that it can still +// access the API. +add_task(async function test_chrome_frame() { + SpecialPowers.pushPrefEnv({ + set: [["security.allow_unsafe_parent_loads", true]], + }); + + await BrowserTestUtils.withNewTab( + `${CHROMEROOT}webapi_checkchromeframe.xhtml`, + async function test_available(browser) { + let available = await check_frame_availability(browser); + ok(available, "API should be available."); + } + ); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js new file mode 100644 index 0000000000..3692644714 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js @@ -0,0 +1,124 @@ +const TESTPAGE = `${SECURE_TESTROOT}webapi_addon_listener.html`; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.webapi.testing", true]], + }); +}); + +async function getListenerEvents(browser) { + let result = await SpecialPowers.spawn(browser, [], async function () { + return content.document.getElementById("result").textContent; + }); + + return result.split("\n").map(JSON.parse); +} + +const RESTARTLESS_ID = "restartless@tests.mozilla.org"; +const INSTALL_ID = "install@tests.mozilla.org"; +const CANCEL_ID = "cancel@tests.mozilla.org"; + +let provider = new MockProvider(); +provider.createAddons([ + { + id: RESTARTLESS_ID, + name: "Restartless add-on", + operationsRequiringRestart: AddonManager.OP_NEED_RESTART_NONE, + }, + { + id: CANCEL_ID, + name: "Add-on for uninstall cancel", + }, +]); + +// Test enable/disable events for restartless +add_task(async function test_restartless() { + await BrowserTestUtils.withNewTab(TESTPAGE, async function (browser) { + let addon = await promiseAddonByID(RESTARTLESS_ID); + is(addon.userDisabled, false, "addon is enabled"); + + // disable it + await addon.disable(); + is(addon.userDisabled, true, "addon was disabled successfully"); + + // re-enable it + await addon.enable(); + is(addon.userDisabled, false, "addon was re-enabled successfuly"); + + let events = await getListenerEvents(browser); + let expected = [ + { id: RESTARTLESS_ID, event: "onDisabling" }, + { id: RESTARTLESS_ID, event: "onDisabled" }, + { id: RESTARTLESS_ID, event: "onEnabling" }, + { id: RESTARTLESS_ID, event: "onEnabled" }, + ]; + Assert.deepEqual(events, expected, "Got expected disable/enable events"); + }); +}); + +// Test install events +add_task(async function test_restartless() { + await BrowserTestUtils.withNewTab(TESTPAGE, async function (browser) { + let addon = new MockAddon( + INSTALL_ID, + "installme", + null, + AddonManager.OP_NEED_RESTART_NONE + ); + let install = new MockInstall(null, null, addon); + + let installPromise = new Promise(resolve => { + install.addTestListener({ + onInstallEnded: resolve, + }); + }); + + provider.addInstall(install); + install.install(); + + await installPromise; + + let events = await getListenerEvents(browser); + let expected = [ + { id: INSTALL_ID, event: "onInstalling" }, + { id: INSTALL_ID, event: "onInstalled" }, + ]; + Assert.deepEqual(events, expected, "Got expected install events"); + }); +}); + +// Test uninstall +add_task(async function test_uninstall() { + await BrowserTestUtils.withNewTab(TESTPAGE, async function (browser) { + let addon = await promiseAddonByID(RESTARTLESS_ID); + isnot(addon, null, "Found add-on for uninstall"); + + addon.uninstall(); + + let events = await getListenerEvents(browser); + let expected = [ + { id: RESTARTLESS_ID, event: "onUninstalling" }, + { id: RESTARTLESS_ID, event: "onUninstalled" }, + ]; + Assert.deepEqual(events, expected, "Got expected uninstall events"); + }); +}); + +// Test cancel of uninstall. +add_task(async function test_cancel() { + await BrowserTestUtils.withNewTab(TESTPAGE, async function (browser) { + let addon = await promiseAddonByID(CANCEL_ID); + isnot(addon, null, "Found add-on for cancelling uninstall"); + + addon.uninstall(); + + let events = await getListenerEvents(browser); + let expected = [{ id: CANCEL_ID, event: "onUninstalling" }]; + Assert.deepEqual(events, expected, "Got expected uninstalling event"); + + addon.cancelUninstall(); + events = await getListenerEvents(browser); + expected.push({ id: CANCEL_ID, event: "onOperationCancelled" }); + Assert.deepEqual(events, expected, "Got expected cancel event"); + }); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_enable.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_enable.js new file mode 100644 index 0000000000..25989bf797 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_enable.js @@ -0,0 +1,63 @@ +const TESTPAGE = `${SECURE_TESTROOT}webapi_addon_listener.html`; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.webapi.testing", true]], + }); +}); + +async function getListenerEvents(browser) { + let result = await SpecialPowers.spawn(browser, [], async function () { + return content.document.getElementById("result").textContent; + }); + + return result.split("\n").map(JSON.parse); +} + +const ID = "test@tests.mozilla.org"; + +let provider = new MockProvider(); +provider.createAddons([ + { + id: ID, + name: "Test add-on", + operationsRequiringRestart: AddonManager.OP_NEED_RESTART_NONE, + }, +]); + +// Test disable and enable from content +add_task(async function () { + await BrowserTestUtils.withNewTab(TESTPAGE, async function (browser) { + let addon = await promiseAddonByID(ID); + isnot(addon, null, "Test addon exists"); + is(addon.userDisabled, false, "addon is enabled"); + + // Disable the addon from content. + await SpecialPowers.spawn(browser, [], async function () { + return content.navigator.mozAddonManager + .getAddonByID("test@tests.mozilla.org") + .then(addon => addon.setEnabled(false)); + }); + + let events = await getListenerEvents(browser); + let expected = [ + { id: ID, event: "onDisabling" }, + { id: ID, event: "onDisabled" }, + ]; + Assert.deepEqual(events, expected, "Got expected disable events"); + + // Enable the addon from content. + await SpecialPowers.spawn(browser, [], async function () { + return content.navigator.mozAddonManager + .getAddonByID("test@tests.mozilla.org") + .then(addon => addon.setEnabled(true)); + }); + + events = await getListenerEvents(browser); + expected = expected.concat([ + { id: ID, event: "onEnabling" }, + { id: ID, event: "onEnabled" }, + ]); + Assert.deepEqual(events, expected, "Got expected enable events"); + }); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js new file mode 100644 index 0000000000..24d34c3f4d --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js @@ -0,0 +1,652 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const { EnterprisePolicyTesting } = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" +); + +const TESTPATH = "webapi_checkavailable.html"; +const TESTPAGE = `${SECURE_TESTROOT}${TESTPATH}`; +const XPI_URL = `${SECURE_TESTROOT}../xpinstall/amosigned.xpi`; +const XPI_ADDON_ID = "amosigned-xpi@tests.mozilla.org"; + +const XPI_SHA = + "sha256:91121ed2c27f670f2307b9aebdd30979f147318c7fb9111c254c14ddbb84e4b0"; + +const ID = "amosigned-xpi@tests.mozilla.org"; +// eh, would be good to just stat the real file instead of this... +const XPI_LEN = 4287; + +AddonTestUtils.initMochitest(this); + +function waitForClear() { + const MSG = "WebAPICleanup"; + return new Promise(resolve => { + let listener = { + receiveMessage(msg) { + if (msg.name == MSG) { + Services.mm.removeMessageListener(MSG, listener); + resolve(); + } + }, + }; + + Services.mm.addMessageListener(MSG, listener, true); + }); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.webapi.testing", true], + ["extensions.install.requireBuiltInCerts", false], + ], + }); + info("added preferences"); +}); + +// Wrapper around a common task to run in the content process to test +// the mozAddonManager API. Takes a URL for the XPI to install and an +// array of steps, each of which can either be an action to take +// (i.e., start or cancel the install) or an install event to wait for. +// Steps that look for a specific event may also include a "props" property +// with properties that the AddonInstall object is expected to have when +// that event is triggered. +async function testInstall(browser, args, steps, description) { + let success = await SpecialPowers.spawn( + browser, + [{ args, steps }], + async function (opts) { + let { args, steps } = opts; + let install = await content.navigator.mozAddonManager.createInstall(args); + if (!install) { + await Promise.reject( + "createInstall() did not return an install object" + ); + } + + // Check that the initial state of the AddonInstall is sane. + if (install.state != "STATE_AVAILABLE") { + await Promise.reject("new install should be in STATE_AVAILABLE"); + } + if (install.error != null) { + await Promise.reject("new install should have null error"); + } + + const events = [ + "onDownloadStarted", + "onDownloadProgress", + "onDownloadEnded", + "onDownloadCancelled", + "onDownloadFailed", + "onInstallStarted", + "onInstallEnded", + "onInstallCancelled", + "onInstallFailed", + ]; + let eventWaiter = null; + let receivedEvents = []; + let prevEvent = null; + events.forEach(event => { + install.addEventListener(event, e => { + receivedEvents.push({ + event, + state: install.state, + error: install.error, + progress: install.progress, + maxProgress: install.maxProgress, + }); + if (eventWaiter) { + eventWaiter(); + } + }); + }); + + // Returns a promise that is resolved when the given event occurs + // or rejects if a different event comes first or if props is supplied + // and properties on the AddonInstall don't match those in props. + function expectEvent(event, props) { + return new Promise((resolve, reject) => { + function check() { + let received = receivedEvents.shift(); + // Skip any repeated onDownloadProgress events. + while ( + received && + received.event == prevEvent && + prevEvent == "onDownloadProgress" + ) { + received = receivedEvents.shift(); + } + // Wait for more events if we skipped all there were. + if (!received) { + eventWaiter = () => { + eventWaiter = null; + check(); + }; + return; + } + prevEvent = received.event; + if (received.event != event) { + let err = new Error( + `expected ${event} but got ${received.event}` + ); + reject(err); + } + if (props) { + for (let key of Object.keys(props)) { + if (received[key] != props[key]) { + throw new Error( + `AddonInstall property ${key} was ${received[key]} but expected ${props[key]}` + ); + } + } + } + resolve(); + } + check(); + }); + } + + while (steps.length) { + let nextStep = steps.shift(); + if (nextStep.action) { + if (nextStep.action == "install") { + try { + await install.install(); + if (nextStep.expectError) { + throw new Error("Expected install to fail but it did not"); + } + } catch (err) { + if (!nextStep.expectError) { + throw new Error("Install failed unexpectedly"); + } + } + } else if (nextStep.action == "cancel") { + await install.cancel(); + } else { + throw new Error(`unknown action ${nextStep.action}`); + } + } else { + await expectEvent(nextStep.event, nextStep.props); + } + } + + return true; + } + ); + + is(success, true, description); +} + +function makeInstallTest(task) { + return async function () { + // withNewTab() will close the test tab before returning, at which point + // the cleanup event will come from the content process. We need to see + // that event but don't want to race to install a listener for it after + // the tab is closed. So set up the listener now but don't yield the + // listening promise until below. + let clearPromise = waitForClear(); + + await BrowserTestUtils.withNewTab(TESTPAGE, task); + + await clearPromise; + is(AddonManager.webAPI.installs.size, 0, "AddonInstall was cleaned up"); + }; +} + +function makeRegularTest(options, what) { + return makeInstallTest(async function (browser) { + let steps = [ + { action: "install" }, + { + event: "onDownloadStarted", + props: { state: "STATE_DOWNLOADING" }, + }, + { + event: "onDownloadProgress", + props: { maxProgress: XPI_LEN }, + }, + { + event: "onDownloadEnded", + props: { + state: "STATE_DOWNLOADED", + progress: XPI_LEN, + maxProgress: XPI_LEN, + }, + }, + { + event: "onInstallStarted", + props: { state: "STATE_INSTALLING" }, + }, + { + event: "onInstallEnded", + props: { state: "STATE_INSTALLED" }, + }, + ]; + + let installPromptPromise = promisePopupNotificationShown( + "addon-webext-permissions" + ).then(panel => { + panel.button.click(); + }); + + let promptPromise = acceptAppMenuNotificationWhenShown( + "addon-installed", + options.addonId + ); + + await testInstall(browser, options, steps, what); + + await installPromptPromise; + + await promptPromise; + + // Sanity check to ensure that the test in makeInstallTest() that + // installs.size == 0 means we actually did clean up. + Assert.greater( + AddonManager.webAPI.installs.size, + 0, + "webAPI is tracking the AddonInstall" + ); + + let addon = await promiseAddonByID(ID); + isnot(addon, null, "Found the addon"); + + // Check that the expected installTelemetryInfo has been stored in the addon details. + AddonTestUtils.checkInstallInfo(addon, { + method: "amWebAPI", + source: "test-host", + sourceURL: /https:\/\/example.com\/.*\/webapi_checkavailable.html/, + }); + + await addon.uninstall(); + + addon = await promiseAddonByID(ID); + is(addon, null, "Addon was uninstalled"); + }); +} + +let addonId = XPI_ADDON_ID; +add_task(makeRegularTest({ url: XPI_URL, addonId }, "a basic install works")); +add_task( + makeRegularTest( + { url: XPI_URL, addonId, hash: null }, + "install with hash=null works" + ) +); +add_task( + makeRegularTest( + { url: XPI_URL, addonId, hash: "" }, + "install with empty string for hash works" + ) +); +add_task( + makeRegularTest( + { url: XPI_URL, addonId, hash: XPI_SHA }, + "install with hash works" + ) +); + +add_task( + makeInstallTest(async function (browser) { + let steps = [ + { action: "cancel" }, + { + event: "onDownloadCancelled", + props: { + state: "STATE_CANCELLED", + error: null, + }, + }, + ]; + + await testInstall( + browser, + { url: XPI_URL }, + steps, + "canceling an install works" + ); + + let addons = await promiseAddonsByIDs([ID]); + is(addons[0], null, "The addon was not installed"); + + Assert.greater( + AddonManager.webAPI.installs.size, + 0, + "webAPI is tracking the AddonInstall" + ); + }) +); + +add_task( + makeInstallTest(async function (browser) { + let steps = [ + { action: "install", expectError: true }, + { + event: "onDownloadStarted", + props: { state: "STATE_DOWNLOADING" }, + }, + { event: "onDownloadProgress" }, + { + event: "onDownloadFailed", + props: { + state: "STATE_DOWNLOAD_FAILED", + error: "ERROR_NETWORK_FAILURE", + }, + }, + ]; + + await testInstall( + browser, + { url: XPI_URL + "bogus" }, + steps, + "install of a bad url fails" + ); + + let addons = await promiseAddonsByIDs([ID]); + is(addons[0], null, "The addon was not installed"); + + Assert.greater( + AddonManager.webAPI.installs.size, + 0, + "webAPI is tracking the AddonInstall" + ); + }) +); + +add_task( + makeInstallTest(async function (browser) { + let steps = [ + { action: "install", expectError: true }, + { + event: "onDownloadStarted", + props: { state: "STATE_DOWNLOADING" }, + }, + { event: "onDownloadProgress" }, + { + event: "onDownloadFailed", + props: { + state: "STATE_DOWNLOAD_FAILED", + error: "ERROR_INCORRECT_HASH", + }, + }, + ]; + + await testInstall( + browser, + { url: XPI_URL, hash: "sha256:bogus" }, + steps, + "install with bad hash fails" + ); + + let addons = await promiseAddonsByIDs([ID]); + is(addons[0], null, "The addon was not installed"); + + Assert.greater( + AddonManager.webAPI.installs.size, + 0, + "webAPI is tracking the AddonInstall" + ); + }) +); + +add_task(async function test_permissions_and_policy() { + async function testBadUrl(url, pattern, successMessage) { + gBrowser.selectedTab = await BrowserTestUtils.addTab(gBrowser, TESTPAGE); + let browser = gBrowser.getBrowserForTab(gBrowser.selectedTab); + await BrowserTestUtils.browserLoaded(browser); + let result = await SpecialPowers.spawn( + browser, + [{ url, pattern }], + function (opts) { + return new Promise(resolve => { + content.navigator.mozAddonManager + .createInstall({ url: opts.url }) + .then( + () => { + resolve({ + success: false, + message: "createInstall should not have succeeded", + }); + }, + err => { + if (err.message.match(new RegExp(opts.pattern))) { + resolve({ success: true }); + } + resolve({ + success: false, + message: `Wrong error message: ${err.message}`, + }); + } + ); + }); + } + ); + is(result.success, true, result.message || successMessage); + } + + await testBadUrl( + "i am not a url", + "NS_ERROR_MALFORMED_URI", + "Installing from an unparseable URL fails" + ); + gBrowser.removeTab(gBrowser.selectedTab); + + let popupPromise = promisePopupNotificationShown( + "addon-install-webapi-blocked" + ); + await Promise.all([ + testBadUrl( + "https://addons.not-really-mozilla.org/impostor.xpi", + "not permitted", + "Installing from non-approved URL fails" + ), + popupPromise, + ]); + + gBrowser.removeTab(gBrowser.selectedTab); + + const blocked_install_message = "Custom Policy Block Message"; + + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + ExtensionSettings: { + "*": { + install_sources: [], + blocked_install_message, + }, + }, + }, + }); + + popupPromise = promisePopupNotificationShown("addon-install-policy-blocked"); + + await testBadUrl( + XPI_URL, + "not permitted by policy", + "Installing from policy blocked origin fails" + ); + + const panel = await popupPromise; + const description = panel.querySelector( + ".popup-notification-description" + ).textContent; + ok( + description.startsWith("Your organization"), + "Policy specific error is shown." + ); + ok( + description.endsWith(` ${blocked_install_message}`), + `Found the expected custom blocked message in "${description}"` + ); + + gBrowser.removeTab(gBrowser.selectedTab); + + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + ExtensionSettings: { + "*": { + install_sources: ["<all_urls>"], + }, + }, + }, + }); +}); + +add_task( + makeInstallTest(async function (browser) { + let xpiURL = `${SECURE_TESTROOT}../xpinstall/incompatible.xpi`; + let id = "incompatible-xpi@tests.mozilla.org"; + + let steps = [ + { action: "install", expectError: true }, + { + event: "onDownloadStarted", + props: { state: "STATE_DOWNLOADING" }, + }, + { event: "onDownloadProgress" }, + { event: "onDownloadEnded" }, + { event: "onDownloadCancelled", error: "ERROR_INCOMPATIBLE" }, + ]; + + await testInstall( + browser, + { url: xpiURL }, + steps, + "install of an incompatible XPI fails" + ); + + let addons = await promiseAddonsByIDs([id]); + is(addons[0], null, "The addon was not installed"); + }) +); + +add_task( + makeInstallTest(async function (browser) { + let id = "amosigned-xpi@tests.mozilla.org"; + let version = "2.1"; + + await AddonTestUtils.loadBlocklistRawData({ + extensionsMLBF: [ + { + stash: { blocked: [`${id}:${version}`], unblocked: [] }, + stash_time: 0, + }, + ], + }); + + let steps = [ + { action: "install", expectError: true }, + { event: "onDownloadStarted" }, + { event: "onDownloadProgress" }, + { event: "onDownloadEnded" }, + { + event: "onDownloadCancelled", + props: { state: "STATE_CANCELLED", error: "ERROR_BLOCKLISTED" }, + }, + ]; + + await testInstall( + browser, + { url: XPI_URL }, + steps, + "install of a blocked XPI fails" + ); + + let addons = await promiseAddonsByIDs([id]); + is(addons[0], null, "The addon was not installed"); + + // Clear the blocklist. + await AddonTestUtils.loadBlocklistRawData({ + extensionsMLBF: [ + { + stash: { blocked: [], unblocked: [] }, + stash_time: 0, + }, + ], + }); + }) +); + +add_task( + makeInstallTest(async function (browser) { + const options = { url: XPI_URL, addonId }; + let steps = [ + { action: "install" }, + { + event: "onDownloadStarted", + props: { state: "STATE_DOWNLOADING" }, + }, + { + event: "onDownloadProgress", + props: { maxProgress: XPI_LEN }, + }, + { + event: "onDownloadEnded", + props: { + state: "STATE_DOWNLOADED", + progress: XPI_LEN, + maxProgress: XPI_LEN, + }, + }, + { + event: "onInstallStarted", + props: { state: "STATE_INSTALLING" }, + }, + { + event: "onInstallEnded", + props: { state: "STATE_INSTALLED" }, + }, + ]; + + await SpecialPowers.spawn(browser, [TESTPATH], testPath => { + // `sourceURL` should match the exact location, even after a location + // update using the history API. In this case, we update the URL with + // query parameters and expect `sourceURL` to contain those parameters. + content.history.pushState( + {}, // state + "", // title + `/${testPath}?some=query&par=am` + ); + }); + + let installPromptPromise = promisePopupNotificationShown( + "addon-webext-permissions" + ).then(panel => { + panel.button.click(); + }); + + let promptPromise = acceptAppMenuNotificationWhenShown( + "addon-installed", + options.addonId + ); + + await Promise.all([ + testInstall(browser, options, steps, "install to check source URL"), + installPromptPromise, + promptPromise, + ]); + + let addon = await promiseAddonByID(ID); + + registerCleanupFunction(async () => { + await addon.uninstall(); + }); + + // Check that the expected installTelemetryInfo has been stored in the + // addon details. + AddonTestUtils.checkInstallInfo(addon, { + method: "amWebAPI", + source: "test-host", + sourceURL: + "https://example.com/webapi_checkavailable.html?some=query&par=am", + }); + }) +); diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_install_disabled.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_install_disabled.js new file mode 100644 index 0000000000..5bc291fe7a --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_install_disabled.js @@ -0,0 +1,60 @@ +const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`; +const XPI_URL = `${SECURE_TESTROOT}../xpinstall/amosigned.xpi`; + +function waitForClear() { + const MSG = "WebAPICleanup"; + return new Promise(resolve => { + let listener = { + receiveMessage(msg) { + if (msg.name == MSG) { + Services.mm.removeMessageListener(MSG, listener); + resolve(); + } + }, + }; + + Services.mm.addMessageListener(MSG, listener, true); + }); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.webapi.testing", true], + ["xpinstall.enabled", false], + ["extensions.install.requireBuiltInCerts", false], + ], + }); + info("added preferences"); +}); + +async function testInstall(browser, args) { + let success = await SpecialPowers.spawn( + browser, + [{ args }], + async function (opts) { + let { args } = opts; + let install; + try { + install = await content.navigator.mozAddonManager.createInstall(args); + } catch (e) {} + return !!install; + } + ); + is(success, false, "Install was blocked"); +} + +add_task(async function () { + // withNewTab() will close the test tab before returning, at which point + // the cleanup event will come from the content process. We need to see + // that event but don't want to race to install a listener for it after + // the tab is closed. So set up the listener now but don't yield the + // listening promise until below. + let clearPromise = waitForClear(); + + await BrowserTestUtils.withNewTab(TESTPAGE, async function (browser) { + await testInstall(browser, { url: XPI_URL }); + }); + + await clearPromise; +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_theme.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_theme.js new file mode 100644 index 0000000000..dd1df90907 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_theme.js @@ -0,0 +1,79 @@ +"use strict"; + +const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`; +const URL = `${SECURE_TESTROOT}addons/browser_theme.xpi`; + +add_task(async function test_theme_install() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.webapi.testing", true], + ["extensions.install.requireBuiltInCerts", false], + ], + }); + + await BrowserTestUtils.withNewTab(TESTPAGE, async browser => { + let updates = []; + function observer(subject, topic, data) { + updates.push(JSON.stringify(subject.wrappedJSObject)); + } + Services.obs.addObserver(observer, "lightweight-theme-styling-update"); + registerCleanupFunction(() => { + Services.obs.removeObserver(observer, "lightweight-theme-styling-update"); + }); + + let sawConfirm = false; + promisePopupNotificationShown("addon-install-confirmation").then(panel => { + sawConfirm = true; + panel.button.click(); + }); + + let prompt1 = waitAppMenuNotificationShown( + "addon-installed", + "theme@tests.mozilla.org", + false + ); + let installPromise = SpecialPowers.spawn(browser, [URL], async url => { + let install = await content.navigator.mozAddonManager.createInstall({ + url, + }); + return install.install(); + }); + await prompt1; + + ok(sawConfirm, "Confirm notification was displayed before installation"); + + // Open a new window and test the app menu panel from there. This verifies the + // incognito checkbox as well as finishing install in this case. + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + await waitAppMenuNotificationShown( + "addon-installed", + "theme@tests.mozilla.org", + true, + newWin + ); + await installPromise; + ok(true, "Theme install completed"); + + await BrowserTestUtils.closeWindow(newWin); + + Assert.equal(updates.length, 1, "Got a single theme update"); + let parsed = JSON.parse(updates[0]); + ok( + parsed.theme.headerURL.endsWith("/testImage.png"), + "Theme update has the expected headerURL" + ); + is( + parsed.theme.id, + "theme@tests.mozilla.org", + "Theme update includes the theme ID" + ); + is( + parsed.theme.version, + "1.0", + "Theme update includes the theme's version" + ); + + let addon = await AddonManager.getAddonByID(parsed.theme.id); + await addon.uninstall(); + }); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_uninstall.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_uninstall.js new file mode 100644 index 0000000000..ad4afe0fa7 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_uninstall.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.webapi.testing", true]], + }); +}); + +function testWithAPI(task) { + return async function () { + await BrowserTestUtils.withNewTab(TESTPAGE, task); + }; +} + +function API_uninstallByID(browser, id) { + return SpecialPowers.spawn(browser, [id], async function (id) { + let addon = await content.navigator.mozAddonManager.getAddonByID(id); + + let result = await addon.uninstall(); + return result; + }); +} + +add_task( + testWithAPI(async function (browser) { + const ID1 = "addon1@tests.mozilla.org"; + const ID2 = "addon2@tests.mozilla.org"; + const ID3 = "addon3@tests.mozilla.org"; + + let provider = new MockProvider(); + + provider.addAddon(new MockAddon(ID1, "Test add-on 1", "extension", 0)); + provider.addAddon(new MockAddon(ID2, "Test add-on 2", "extension", 0)); + + let addon = new MockAddon(ID3, "Test add-on 3", "extension", 0); + addon.permissions &= ~AddonManager.PERM_CAN_UNINSTALL; + provider.addAddon(addon); + + let [a1, a2, a3] = await promiseAddonsByIDs([ID1, ID2, ID3]); + isnot(a1, null, "addon1 is installed"); + isnot(a2, null, "addon2 is installed"); + isnot(a3, null, "addon3 is installed"); + + let result = await API_uninstallByID(browser, ID1); + is(result, true, "uninstall of addon1 succeeded"); + + [a1, a2, a3] = await promiseAddonsByIDs([ID1, ID2, ID3]); + is(a1, null, "addon1 is uninstalled"); + isnot(a2, null, "addon2 is still installed"); + + result = await API_uninstallByID(browser, ID2); + is(result, true, "uninstall of addon2 succeeded"); + [a2] = await promiseAddonsByIDs([ID2]); + is(a2, null, "addon2 is uninstalled"); + + await Assert.rejects( + API_uninstallByID(browser, ID3), + /Addon cannot be uninstalled/, + "Unable to uninstall addon" + ); + + // Cleanup addon3 + a3.permissions |= AddonManager.PERM_CAN_UNINSTALL; + await a3.uninstall(); + [a3] = await promiseAddonsByIDs([ID3]); + is(a3, null, "addon3 is uninstalled"); + }) +); diff --git a/toolkit/mozapps/extensions/test/browser/browser_webext_icon.js b/toolkit/mozapps/extensions/test/browser/browser_webext_icon.js new file mode 100644 index 0000000000..123fe0c665 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_webext_icon.js @@ -0,0 +1,82 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function pngArrayBuffer(size) { + const canvas = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "canvas" + ); + canvas.height = canvas.width = size; + const ctx = canvas.getContext("2d"); + ctx.fillStyle = "blue"; + ctx.fillRect(0, 0, size, size); + return new Promise(resolve => { + canvas.toBlob(blob => { + const fileReader = new FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blob); + }); + }); +} + +async function checkIconInView(view, name, findIcon) { + const manager = await open_manager(view); + const icon = findIcon(manager.document); + const size = Number(icon.src.match(/icon(\d+)\.png/)[1]); + is( + icon.clientWidth, + icon.clientHeight, + `The icon should be square in ${name}` + ); + is( + size, + icon.clientWidth * window.devicePixelRatio, + `The correct icon size should have been chosen in ${name}` + ); + await close_manager(manager); +} + +add_task(async function test_addon_icon() { + // This test loads an extension with a variety of icon sizes, and checks that the + // fitting one is chosen. If this fails it's because you changed the icon size in + // about:addons but didn't update some AddonManager.getPreferredIconURL call. + const id = "@test-addon-icon"; + const icons = {}; + const files = {}; + const file = await pngArrayBuffer(256); + for (let size = 1; size <= 256; ++size) { + let fileName = `icon${size}.png`; + icons[size] = fileName; + files[fileName] = file; + } + const extensionDefinition = { + useAddonManager: "temporary", + manifest: { + browser_specific_settings: { + gecko: { id }, + }, + icons, + }, + files, + }; + + const extension = ExtensionTestUtils.loadExtension(extensionDefinition); + await extension.startup(); + + await checkIconInView("addons://list/extension", "list", doc => { + return getAddonCard(doc.defaultView, id).querySelector(".addon-icon"); + }); + + await checkIconInView( + "addons://detail/" + encodeURIComponent(id), + "details", + doc => { + return getAddonCard(doc.defaultView, id).querySelector(".addon-icon"); + } + ); + + await extension.unload(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_webext_incognito.js b/toolkit/mozapps/extensions/test/browser/browser_webext_incognito.js new file mode 100644 index 0000000000..9180bbcf91 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_webext_incognito.js @@ -0,0 +1,593 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { ExtensionPermissions } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionPermissions.sys.mjs" +); +const { Management } = ChromeUtils.importESModule( + "resource://gre/modules/Extension.sys.mjs" +); + +var gManagerWindow; + +AddonTestUtils.initMochitest(this); + +function get_test_items() { + var items = {}; + + for (let item of gManagerWindow.document.querySelectorAll("addon-card")) { + items[item.getAttribute("addon-id")] = item; + } + + return items; +} + +function getHtmlElem(selector) { + return gManagerWindow.document.querySelector(selector); +} + +function getPrivateBrowsingBadge(card) { + return card.querySelector(".addon-badge-private-browsing-allowed"); +} + +function getPreferencesButtonAtListView(card) { + return card.querySelector("panel-item[action='preferences']"); +} + +function getPreferencesButtonAtDetailsView() { + return getHtmlElem("panel-item[action='preferences']"); +} + +function isInlineOptionsVisible() { + // The following button is used to open the inline options browser. + return !getHtmlElem(".tab-button[name='preferences']").hidden; +} + +function getPrivateBrowsingValue() { + return getHtmlElem("input[type='radio'][name='private-browsing']:checked") + .value; +} + +async function setPrivateBrowsingValue(value, id) { + let changePromise = new Promise(resolve => { + const listener = (type, { extensionId, added, removed }) => { + if (extensionId == id) { + // Let's make sure we received the right message + let { permissions } = value == "0" ? removed : added; + ok(permissions.includes("internal:privateBrowsingAllowed")); + Management.off("change-permissions", listener); + resolve(); + } + }; + Management.on("change-permissions", listener); + }); + let radio = getHtmlElem( + `input[type="radio"][name="private-browsing"][value="${value}"]` + ); + // NOTE: not using EventUtils.synthesizeMouseAtCenter here because it + // does make this test to fail intermittently in some jobs (e.g. TV jobs) + radio.click(); + // Let's make sure we wait until the change has peristed in the database + return changePromise; +} + +// Check whether the private browsing inputs are visible in the details view. +function checkIsModifiable(expected) { + if (expected) { + is_element_visible( + getHtmlElem(".addon-detail-row-private-browsing"), + "Private browsing should be visible" + ); + } else { + is_element_hidden( + getHtmlElem(".addon-detail-row-private-browsing"), + "Private browsing should be hidden" + ); + } + checkHelpRow(".addon-detail-row-private-browsing", expected); +} + +// Check whether the details view shows that private browsing is forcibly disallowed. +function checkIsDisallowed(expected) { + if (expected) { + is_element_visible( + getHtmlElem(".addon-detail-row-private-browsing-disallowed"), + "Private browsing should be disallowed" + ); + } else { + is_element_hidden( + getHtmlElem(".addon-detail-row-private-browsing-disallowed"), + "Private browsing should not be disallowed" + ); + } + checkHelpRow(".addon-detail-row-private-browsing-disallowed", expected); +} + +// Check whether the details view shows that private browsing is forcibly allowed. +function checkIsRequired(expected) { + if (expected) { + is_element_visible( + getHtmlElem(".addon-detail-row-private-browsing-required"), + "Private browsing should be required" + ); + } else { + is_element_hidden( + getHtmlElem(".addon-detail-row-private-browsing-required"), + "Private browsing should not be required" + ); + } + checkHelpRow(".addon-detail-row-private-browsing-required", expected); +} + +function checkHelpRow(selector, expected) { + let helpRow = getHtmlElem(`${selector} + .addon-detail-help-row`); + if (expected) { + is_element_visible(helpRow, `Help row should be shown: ${selector}`); + is_element_visible(helpRow.querySelector("a"), "Expected learn more link"); + } else { + is_element_hidden(helpRow, `Help row should be hidden: ${selector}`); + } +} + +async function hasPrivateAllowed(id) { + let perms = await ExtensionPermissions.get(id); + return perms.permissions.includes("internal:privateBrowsingAllowed"); +} + +add_task(async function test_badge_and_toggle_incognito() { + let addons = new Map([ + [ + "@test-default", + { + useAddonManager: "temporary", + manifest: { + browser_specific_settings: { + gecko: { id: "@test-default" }, + }, + }, + }, + ], + [ + "@test-override", + { + useAddonManager: "temporary", + manifest: { + browser_specific_settings: { + gecko: { id: "@test-override" }, + }, + }, + incognitoOverride: "spanning", + }, + ], + [ + "@test-override-permanent", + { + useAddonManager: "permanent", + manifest: { + browser_specific_settings: { + gecko: { id: "@test-override-permanent" }, + }, + }, + incognitoOverride: "spanning", + }, + ], + [ + "@test-not-allowed", + { + useAddonManager: "temporary", + manifest: { + browser_specific_settings: { + gecko: { id: "@test-not-allowed" }, + }, + incognito: "not_allowed", + }, + }, + ], + ]); + let extensions = []; + for (let definition of addons.values()) { + let extension = ExtensionTestUtils.loadExtension(definition); + extensions.push(extension); + await extension.startup(); + } + + gManagerWindow = await open_manager("addons://list/extension"); + let items = get_test_items(); + for (let [id, definition] of addons.entries()) { + ok(items[id], `${id} listed`); + let badge = getPrivateBrowsingBadge(items[id]); + if (definition.incognitoOverride == "spanning") { + is_element_visible(badge, `private browsing badge is visible`); + } else { + is_element_hidden(badge, `private browsing badge is hidden`); + } + } + await close_manager(gManagerWindow); + + for (let [id, definition] of addons.entries()) { + gManagerWindow = await open_manager( + "addons://detail/" + encodeURIComponent(id) + ); + ok(true, `==== ${id} detail opened`); + if (definition.manifest.incognito == "not_allowed") { + checkIsModifiable(false); + ok(!(await hasPrivateAllowed(id)), "Private browsing permission not set"); + checkIsDisallowed(true); + } else { + // This assumes PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS, we test other options in a later test in this file. + checkIsModifiable(true); + if (definition.incognitoOverride == "spanning") { + is(getPrivateBrowsingValue(), "1", "Private browsing should be on"); + ok(await hasPrivateAllowed(id), "Private browsing permission set"); + await setPrivateBrowsingValue("0", id); + is(getPrivateBrowsingValue(), "0", "Private browsing should be off"); + ok( + !(await hasPrivateAllowed(id)), + "Private browsing permission removed" + ); + } else { + is(getPrivateBrowsingValue(), "0", "Private browsing should be off"); + ok( + !(await hasPrivateAllowed(id)), + "Private browsing permission not set" + ); + await setPrivateBrowsingValue("1", id); + is(getPrivateBrowsingValue(), "1", "Private browsing should be on"); + ok(await hasPrivateAllowed(id), "Private browsing permission set"); + } + } + await close_manager(gManagerWindow); + } + + for (let extension of extensions) { + await extension.unload(); + } +}); + +add_task(async function test_addon_preferences_button() { + let addons = new Map([ + [ + "test-inline-options@mozilla.com", + { + useAddonManager: "temporary", + manifest: { + name: "Extension with inline options", + browser_specific_settings: { + gecko: { id: "test-inline-options@mozilla.com" }, + }, + options_ui: { page: "options.html", open_in_tab: false }, + }, + }, + ], + [ + "test-newtab-options@mozilla.com", + { + useAddonManager: "temporary", + manifest: { + name: "Extension with options page in a new tab", + browser_specific_settings: { + gecko: { id: "test-newtab-options@mozilla.com" }, + }, + options_ui: { page: "options.html", open_in_tab: true }, + }, + }, + ], + [ + "test-not-allowed@mozilla.com", + { + useAddonManager: "temporary", + manifest: { + name: "Extension not allowed in PB windows", + incognito: "not_allowed", + browser_specific_settings: { + gecko: { id: "test-not-allowed@mozilla.com" }, + }, + options_ui: { page: "options.html", open_in_tab: true }, + }, + }, + ], + ]); + + async function runTest(openInPrivateWin) { + const win = await BrowserTestUtils.openNewBrowserWindow({ + private: openInPrivateWin, + }); + + gManagerWindow = await open_manager( + "addons://list/extension", + undefined, + undefined, + undefined, + win + ); + + const checkPrefsVisibility = (id, hasInlinePrefs, expectVisible) => { + if (!hasInlinePrefs) { + const detailsPrefBtn = getPreferencesButtonAtDetailsView(); + is( + !detailsPrefBtn.hidden, + expectVisible, + `The ${id} prefs button in the addon details has the expected visibility` + ); + } else { + is( + isInlineOptionsVisible(), + expectVisible, + `The ${id} inline prefs in the addon details has the expected visibility` + ); + } + }; + + const setAddonPrivateBrowsingAccess = async (id, allowPrivateBrowsing) => { + const cardUpdatedPromise = BrowserTestUtils.waitForEvent( + getHtmlElem("addon-card"), + "update" + ); + is( + getPrivateBrowsingValue(), + allowPrivateBrowsing ? "0" : "1", + `Private browsing should be initially ${ + allowPrivateBrowsing ? "off" : "on" + }` + ); + + // Get the DOM element we want to click on (to allow or disallow the + // addon on private browsing windows). + await setPrivateBrowsingValue(allowPrivateBrowsing ? "1" : "0", id); + + info(`Waiting for details view of ${id} to be reloaded`); + await cardUpdatedPromise; + + is( + getPrivateBrowsingValue(), + allowPrivateBrowsing ? "1" : "0", + `Private browsing should be initially ${ + allowPrivateBrowsing ? "on" : "off" + }` + ); + + is( + await hasPrivateAllowed(id), + allowPrivateBrowsing, + `Private browsing permission ${ + allowPrivateBrowsing ? "added" : "removed" + }` + ); + let badge = getPrivateBrowsingBadge(getHtmlElem("addon-card")); + is( + !badge.hidden, + allowPrivateBrowsing, + `Expected private browsing badge at ${id}` + ); + }; + + const extensions = []; + for (const definition of addons.values()) { + const extension = ExtensionTestUtils.loadExtension(definition); + extensions.push(extension); + await extension.startup(); + } + + const items = get_test_items(); + + for (const id of addons.keys()) { + // Check the preferences button in the addon list page. + is( + getPreferencesButtonAtListView(items[id]).hidden, + openInPrivateWin, + `The ${id} prefs button in the addon list has the expected visibility` + ); + } + + for (const [id, definition] of addons.entries()) { + // Check the preferences button or inline frame in the addon + // details page. + info(`Opening addon details for ${id}`); + const hasInlinePrefs = !definition.manifest.options_ui.open_in_tab; + const onceViewChanged = wait_for_view_load(gManagerWindow, null, true); + gManagerWindow.loadView(`addons://detail/${encodeURIComponent(id)}`); + await onceViewChanged; + + checkPrefsVisibility(id, hasInlinePrefs, !openInPrivateWin); + + // While testing in a private window, also check that the preferences + // are going to be visible when we toggle the PB access for the addon. + if (openInPrivateWin && definition.manifest.incognito !== "not_allowed") { + await setAddonPrivateBrowsingAccess(id, true); + checkPrefsVisibility(id, hasInlinePrefs, true); + + await setAddonPrivateBrowsingAccess(id, false); + checkPrefsVisibility(id, hasInlinePrefs, false); + } + } + + for (const extension of extensions) { + await extension.unload(); + } + + await close_manager(gManagerWindow); + await BrowserTestUtils.closeWindow(win); + } + + // run tests in private and non-private windows. + await runTest(true); + await runTest(false); +}); + +add_task(async function test_addon_postinstall_incognito_hidden_checkbox() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.langpacks.signatures.required", false]], + }); + + const TEST_ADDONS = [ + { + manifest: { + name: "Extension incognito default opt-in", + browser_specific_settings: { + gecko: { id: "ext-incognito-default-opt-in@mozilla.com" }, + }, + }, + }, + { + manifest: { + name: "Extension incognito not_allowed", + browser_specific_settings: { + gecko: { id: "ext-incognito-not-allowed@mozilla.com" }, + }, + incognito: "not_allowed", + }, + }, + { + manifest: { + name: "Static Theme", + browser_specific_settings: { + gecko: { id: "static-theme@mozilla.com" }, + }, + theme: { + colors: { + frame: "#FFFFFF", + tab_background_text: "#000", + }, + }, + }, + }, + { + manifest: { + name: "Dictionary", + browser_specific_settings: { gecko: { id: "dictionary@mozilla.com" } }, + dictionaries: { + und: "dictionaries/und.dic", + }, + }, + files: { + "dictionaries/und.dic": "", + "dictionaries/und.aff": "", + }, + }, + { + manifest: { + name: "Langpack", + browser_specific_settings: { gecko: { id: "langpack@mozilla.com" } }, + langpack_id: "und", + languages: { + und: { + chrome_resources: { + global: "chrome/und/locale/und/global", + }, + version: "20190326174300", + }, + }, + }, + }, + ]; + + for (let definition of TEST_ADDONS) { + let { id } = definition.manifest.browser_specific_settings.gecko; + info( + `Testing incognito checkbox visibility on ${id} post install notification` + ); + + const xpi = AddonTestUtils.createTempWebExtensionFile(definition); + let install = await AddonManager.getInstallForFile(xpi); + + await Promise.all([ + waitAppMenuNotificationShown("addon-installed", id, true), + install.install().then(() => { + Services.obs.notifyObservers( + { + addon: install.addon, + target: gBrowser.selectedBrowser, + }, + "webextension-install-notify" + ); + }), + ]); + + const { addon } = install; + const { permissions } = addon; + const canChangePBAccess = Boolean( + permissions & AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS + ); + + if (id === "ext-incognito-default-opt-in@mozilla.com") { + ok( + canChangePBAccess, + `${id} should have the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission` + ); + } else { + ok( + !canChangePBAccess, + `${id} should not have the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission` + ); + } + + // This tests the visibility of various private detail rows. + gManagerWindow = await open_manager( + "addons://detail/" + encodeURIComponent(id) + ); + info(`addon ${id} detail opened`); + if (addon.type === "extension") { + checkIsModifiable(canChangePBAccess); + let required = addon.incognito === "spanning"; + checkIsRequired(!canChangePBAccess && required); + checkIsDisallowed(!canChangePBAccess && !required); + } else { + checkIsModifiable(false); + checkIsRequired(false); + checkIsDisallowed(false); + } + await close_manager(gManagerWindow); + + await addon.uninstall(); + } + + // It is not possible to create a privileged add-on and install it, so just + // simulate an installed privileged add-on and check the UI. + await test_incognito_of_privileged_addons(); +}); + +// Checks that the private browsing flag of privileged add-ons cannot be modified. +async function test_incognito_of_privileged_addons() { + // In mochitests it is not possible to create and install a privileged add-on + // or a system add-on, so create a mock provider that simulates privileged + // add-ons (which lack the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission). + let provider = new MockProvider(); + provider.createAddons([ + { + name: "default incognito", + id: "default-incognito@mock", + incognito: "spanning", // This is the default. + // Anything without the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission. + permissions: 0, + }, + { + name: "not_allowed incognito", + id: "not-allowed-incognito@mock", + incognito: "not_allowed", + // Anything without the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission. + permissions: 0, + }, + ]); + + gManagerWindow = await open_manager( + "addons://detail/default-incognito%40mock" + ); + checkIsModifiable(false); + checkIsRequired(true); + checkIsDisallowed(false); + await close_manager(gManagerWindow); + + gManagerWindow = await open_manager( + "addons://detail/not-allowed-incognito%40mock" + ); + checkIsModifiable(false); + checkIsRequired(false); + checkIsDisallowed(true); + await close_manager(gManagerWindow); + + provider.unregister(); +} diff --git a/toolkit/mozapps/extensions/test/browser/discovery/api_response.json b/toolkit/mozapps/extensions/test/browser/discovery/api_response.json new file mode 100644 index 0000000000..b36d3c1f02 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/discovery/api_response.json @@ -0,0 +1,679 @@ +{ + "results": [ + { + "description_text": "", + "addon": { + "icon_url": "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png", + "guid": "{e0d2e13b-2e07-49d5-9574-eb0227482320}", + "authors": [ + { + "id": 7804538, + "name": "Sondergaard", + "picture_url": "https://addons-dev-cdn.allizom.org/user-media/userpics/7/7804/7804538.png?modified=1392125542", + "username": "EatingStick", + "url": "https://addons-dev.allizom.org/en-US/firefox/user/7804538/" + } + ], + "previews": [ + { + "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183758.png?modified=1555593109", + "image_size": [680, 92], + "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183758.png?modified=1555593109", + "id": 183758, + "thumbnail_size": [473, 64], + "caption": null + }, + { + "id": 183768, + "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183768.png?modified=1555593111", + "image_size": [760, 92], + "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183768.png?modified=1555593111", + "caption": null, + "thumbnail_size": [529, 64] + }, + { + "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183777.png?modified=1555593112", + "id": 183777, + "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183777.png?modified=1555593112", + "image_size": [720, 92], + "caption": null, + "thumbnail_size": [501, 64] + } + ], + "name": "Tigers Matter ** DON'T DELTE ME**", + "id": 496012, + "url": "https://addons-dev.allizom.org/en-US/firefox/addon/tigers-matter/", + "type": "statictheme", + "ratings": { + "average": 4.7636, + "text_count": 55, + "count": 55, + "bayesian_average": 4.75672 + }, + "slug": "tigers-matter", + "average_daily_users": 1, + "current_version": { + "compatibility": { + "firefox": { + "max": "*", + "min": "53.0" + }, + "android": { + "max": "*", + "min": "65.0" + } + }, + "is_strict_compatibility_enabled": false, + "id": 1655900, + "files": [ + { + "is_restart_required": false, + "url": "https://addons-dev.allizom.org/firefox/downloads/file/376561/tigers_matter_dont_delte_me-2.0-an+fx.xpi?src=", + "created": "2019-04-18T13:11:48Z", + "size": 86337, + "status": "public", + "is_webextension": true, + "is_mozilla_signed_extension": false, + "permissions": [], + "hash": "sha256:ebeb6e4f40ceafbc4affc5bc9a182ed44ae410d71b8c5f9c547f8d45863e0c37", + "platform": "all", + "id": 376561 + } + ] + } + }, + "is_recommendation": false + }, + { + "is_recommendation": false, + "addon": { + "url": "https://addons-dev.allizom.org/en-US/firefox/addon/awesome-screenshot-plus-/", + "type": "extension", + "ratings": { + "count": 848, + "bayesian_average": 3.87925, + "average": 3.8797, + "text_count": 842 + }, + "slug": "awesome-screenshot-plus-", + "average_daily_users": 1, + "current_version": { + "is_strict_compatibility_enabled": false, + "id": 1532816, + "files": [ + { + "url": "https://addons-dev.allizom.org/firefox/downloads/file/253549/awesome_screenshot_plus-7-an+fx.xpi?src=", + "is_restart_required": false, + "size": 4196, + "created": "2017-09-01T13:31:17Z", + "is_webextension": true, + "status": "public", + "is_mozilla_signed_extension": false, + "permissions": [], + "hash": "sha256:4cd8e9b7e89f61e6855d01c73c5c05920c1e0e91f3ae0f45adbb4bd9919f59d7", + "platform": "all", + "id": 253549 + } + ], + "compatibility": { + "android": { + "min": "48.0", + "max": "*" + }, + "firefox": { + "max": "*", + "min": "48.0" + } + } + }, + "authors": [ + { + "username": "diigo-inc", + "name": "Diigo Inc.", + "picture_url": "https://addons-dev-cdn.allizom.org/user-media/userpics/0/6/6724.png?modified=1554393597", + "url": "https://addons-dev.allizom.org/en-US/firefox/user/6724/", + "id": 6724 + } + ], + "icon_url": "https://addons-dev-cdn.allizom.org/user-media/addon_icons/287/287841-64.png?modified=mcrushed", + "guid": "jid0-GXjLLfbCoAx0LcltEdFrEkQdQPI@jetpack", + "previews": [ + { + "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/previews/thumbs/54/54638.png?modified=1543388383", + "id": 54638, + "image_size": [625, 525], + "image_url": "https://addons-dev-cdn.allizom.org/user-media/previews/full/54/54638.png?modified=1543388383", + "caption": "Capture and annotate a page", + "thumbnail_size": [571, 480] + }, + { + "caption": "Crop selected area", + "thumbnail_size": [571, 480], + "image_url": "https://addons-dev-cdn.allizom.org/user-media/previews/full/54/54639.png?modified=1543388385", + "image_size": [625, 525], + "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/previews/thumbs/54/54639.png?modified=1543388385", + "id": 54639 + }, + { + "caption": "Save as a local file or upload to get a sharable link", + "thumbnail_size": [640, 234], + "image_url": "https://addons-dev-cdn.allizom.org/user-media/previews/full/54/54641.png?modified=1543388385", + "image_size": [700, 256], + "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/previews/thumbs/54/54641.png?modified=1543388385", + "id": 54641 + } + ], + "name": "Awesome Screenshot Plus - Capture, Annotate & More", + "id": 287841 + }, + "description_text": "Capture the whole page or any portion, annotate it with rectangles, circles, arrows, lines and text, blur sensitive info, one-click upload to share. And more! Capture the whole page or any portion, annotate it with rectangles, circles, arrows, lines" + }, + { + "description_text": "Help Admins in their daily work", + "addon": { + "slug": "amo-admin-assistant-test", + "average_daily_users": 0, + "current_version": { + "files": [ + { + "is_restart_required": false, + "url": "https://addons-dev.allizom.org/firefox/downloads/file/255370/amo_admin_assistant-4.2-fx.xpi?src=", + "size": 16016, + "created": "2018-08-21T16:49:21Z", + "is_webextension": true, + "status": "public", + "is_mozilla_signed_extension": false, + "permissions": [ + "tabs", + "https://addons-internal.prod.mozaws.net/*" + ], + "hash": "sha256:cd28c841a6daf8a2e3c94b0773b373fec0213404b70074309326cfc75e6725d3", + "platform": "all", + "id": 255370 + } + ], + "is_strict_compatibility_enabled": false, + "id": 1534709, + "compatibility": { + "firefox": { + "min": "45.0", + "max": "*" + } + } + }, + "url": "https://addons-dev.allizom.org/en-US/firefox/addon/amo-admin-assistant-test/", + "ratings": { + "bayesian_average": 0, + "count": 0, + "text_count": 0, + "average": 0 + }, + "type": "extension", + "id": 496168, + "guid": "aaa-test-icon@xulforge.com", + "icon_url": "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png", + "authors": [ + { + "id": 4230, + "url": "https://addons-dev.allizom.org/en-US/firefox/user/4230/", + "username": "jorge-villalobos", + "name": "Jorge Villalobos", + "picture_url": null + } + ], + "previews": [], + "name": "AMO Admin Assistant Test" + }, + "is_recommendation": false + }, + { + "addon": { + "authors": [ + { + "name": "LexaDev", + "picture_url": "https://addons-dev-cdn.allizom.org/user-media/userpics/10/10640/10640485.png?modified=1554812253", + "username": "LexaSV", + "url": "https://addons-dev.allizom.org/en-US/firefox/user/10640485/", + "id": 10640485 + } + ], + "icon_url": "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png", + "guid": "{f9b9cdd3-91ae-476e-9c21-d5ecfce9889f}", + "previews": [ + { + "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183694.png?modified=1555593096", + "image_size": [680, 92], + "id": 183694, + "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183694.png?modified=1555593096", + "thumbnail_size": [473, 64], + "caption": null + }, + { + "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183699.png?modified=1555593097", + "id": 183699, + "image_size": [760, 92], + "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183699.png?modified=1555593097", + "caption": null, + "thumbnail_size": [529, 64] + }, + { + "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183703.png?modified=1555593098", + "image_size": [720, 92], + "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183703.png?modified=1555593098", + "id": 183703, + "caption": null, + "thumbnail_size": [501, 64] + } + ], + "name": "iarba", + "id": 495969, + "url": "https://addons-dev.allizom.org/en-US/firefox/addon/iarba/", + "ratings": { + "bayesian_average": 4.86128, + "count": 10, + "text_count": 10, + "average": 4.9 + }, + "type": "statictheme", + "slug": "iarba", + "current_version": { + "files": [ + { + "url": "https://addons-dev.allizom.org/firefox/downloads/file/376535/iarba-2.0-an+fx.xpi?src=", + "is_restart_required": false, + "size": 895804, + "created": "2019-04-18T13:11:35Z", + "is_mozilla_signed_extension": false, + "status": "public", + "is_webextension": true, + "id": 376535, + "permissions": [], + "platform": "all", + "hash": "sha256:d7ecbdfa8ba56c5d08129c867e68b02ffc8c6000a7f7f85d85d2a558045babfa" + } + ], + "is_strict_compatibility_enabled": false, + "id": 1655874, + "compatibility": { + "android": { + "min": "65.0", + "max": "*" + }, + "firefox": { + "min": "53.0", + "max": "*" + } + } + }, + "average_daily_users": 1 + }, + "description_text": "", + "is_recommendation": false + }, + { + "description_text": "Get international weather forecasts", + "addon": { + "id": 502855, + "authors": [ + { + "id": 10641527, + "url": "https://addons-dev.allizom.org/en-US/firefox/user/10641527/", + "name": "Amoga-dev", + "picture_url": "https://addons-dev-cdn.allizom.org/user-media/userpics/10/10641/10641527.png?modified=1555333028", + "username": "Amoga_dev_REST" + } + ], + "icon_url": "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png", + "guid": "forecastfox@s3_fix_version", + "previews": [], + "name": "Forecastfox (fix version)", + "slug": "forecastfox-fix-version", + "current_version": { + "id": 1541667, + "is_strict_compatibility_enabled": false, + "files": [ + { + "permissions": [ + "activeTab", + "tabs", + "background", + "storage", + "webRequest", + "webRequestBlocking", + "<all_urls>", + "http://www.s3blog.org/geolocation.html*", + "https://embed.windy.com/embed2.html*" + ], + "platform": "all", + "hash": "sha256:89e4de4ce86005c57b0197f671e86936aaf843ebd5751caae02cad4991ccbf0a", + "id": 262328, + "is_webextension": true, + "status": "public", + "is_mozilla_signed_extension": false, + "url": "https://addons-dev.allizom.org/firefox/downloads/file/262328/forecastfox_fix_version-4.20-an+fx.xpi?src=", + "is_restart_required": false, + "created": "2019-01-16T07:54:26Z", + "size": 1331686 + } + ], + "compatibility": { + "android": { + "min": "51.0", + "max": "*" + }, + "firefox": { + "min": "51.0", + "max": "*" + } + } + }, + "average_daily_users": 0, + "url": "https://addons-dev.allizom.org/en-US/firefox/addon/forecastfox-fix-version/", + "type": "extension", + "ratings": { + "count": 0, + "bayesian_average": 0, + "average": 0, + "text_count": 0 + } + }, + "is_recommendation": false + }, + { + "description_text": "A test extension from webext-generator.", + "addon": { + "name": "tabby cat", + "previews": [], + "guid": "{1ed4b641-bac7-4492-b304-6ddc01f538ae}", + "icon_url": "https://addons-dev-cdn.allizom.org/user-media/addon_icons/502/502774-64.png?modified=f289a992", + "authors": [ + { + "url": "https://addons-dev.allizom.org/en-US/firefox/user/10641572/", + "username": "AdminUserTestDev1", + "picture_url": "https://addons-dev-cdn.allizom.org/user-media/userpics/10/10641/10641572.png?modified=1555675110", + "name": "úþÿ Ψ Φ ֎", + "id": 10641572 + } + ], + "id": 502774, + "ratings": { + "bayesian_average": 0, + "count": 0, + "text_count": 0, + "average": 0 + }, + "type": "extension", + "url": "https://addons-dev.allizom.org/en-US/firefox/addon/tabby-catextension/", + "current_version": { + "compatibility": { + "firefox": { + "max": "*", + "min": "48.0" + }, + "android": { + "max": "*", + "min": "48.0" + } + }, + "is_strict_compatibility_enabled": false, + "id": 1541570, + "files": [ + { + "created": "2018-12-04T09:54:24Z", + "size": 4374, + "is_restart_required": false, + "url": "https://addons-dev.allizom.org/firefox/downloads/file/262231/tabby_cat-1.0-an+fx.xpi?src=", + "is_mozilla_signed_extension": false, + "status": "public", + "is_webextension": true, + "id": 262231, + "hash": "sha256:f12c8a8b71e7d4c48e38db6b6a374ca8dcde42d6cb13fb1f2a8299bb51116615", + "platform": "all", + "permissions": [] + } + ] + }, + "average_daily_users": 1, + "slug": "tabby-catextension" + }, + "is_recommendation": false + }, + { + "addon": { + "url": "https://addons-dev.allizom.org/en-US/firefox/addon/the-moon-cat/", + "ratings": { + "average": 4.8182, + "text_count": 11, + "count": 11, + "bayesian_average": 4.78325 + }, + "type": "statictheme", + "slug": "the-moon-cat", + "average_daily_users": 2, + "current_version": { + "files": [ + { + "is_mozilla_signed_extension": false, + "status": "public", + "is_webextension": true, + "id": 262333, + "permissions": [], + "hash": "sha256:d159190add69c739b0fe07b19ad3ff48045c5ded502a8df0f892b8feb645c5ae", + "platform": "all", + "is_restart_required": false, + "url": "https://addons-dev.allizom.org/firefox/downloads/file/262333/the_moon_cat-1.0-an+fx.xpi?src=", + "size": 102889, + "created": "2019-01-16T08:31:21Z" + } + ], + "is_strict_compatibility_enabled": false, + "id": 1541672, + "compatibility": { + "firefox": { + "max": "*", + "min": "53.0" + }, + "android": { + "min": "65.0", + "max": "*" + } + } + }, + "icon_url": "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png", + "authors": [ + { + "url": "https://addons-dev.allizom.org/en-US/firefox/user/5822165/", + "username": "Rallara", + "name": "Rallara", + "picture_url": "https://addons-dev-cdn.allizom.org/user-media/userpics/5/5822/5822165.png?modified=1391855104", + "id": 5822165 + } + ], + "guid": "{db4f6548-da04-43fb-a03e-249bf70ef5a1}", + "previews": [ + { + "thumbnail_size": [473, 64], + "caption": null, + "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14307.png?modified=1547627485", + "image_size": [680, 92], + "id": 14307, + "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14307.png?modified=1547627485" + }, + { + "thumbnail_size": [529, 64], + "caption": null, + "id": 14308, + "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14308.png?modified=1547627486", + "image_size": [760, 92], + "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14308.png?modified=1547627486" + }, + { + "thumbnail_size": [501, 64], + "caption": null, + "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14309.png?modified=1547627487", + "image_size": [720, 92], + "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14309.png?modified=1547627487", + "id": 14309 + } + ], + "name": "the Moon Cat", + "id": 502859 + }, + "description_text": "", + "is_recommendation": false + }, + { + "is_recommendation": false, + "addon": { + "icon_url": "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png", + "guid": "{2e5ff8c8-32fe-46d0-9fc8-6b8986621f3c}", + "authors": [ + { + "id": 10641570, + "url": "https://addons-dev.allizom.org/en-US/firefox/user/10641570/", + "name": "BobsDisplayName", + "picture_url": "https://addons-dev-cdn.allizom.org/user-media/userpics/10/10641/10641570.png?modified=1536063975", + "username": "BobsUserName" + } + ], + "previews": [], + "name": "SI", + "id": 495710, + "url": "https://addons-dev.allizom.org/en-US/firefox/addon/search_by_image/", + "ratings": { + "average": 3.8333, + "text_count": 5, + "count": 6, + "bayesian_average": 3.77144 + }, + "type": "extension", + "slug": "search_by_image", + "current_version": { + "files": [ + { + "id": 262271, + "permissions": [ + "contextMenus", + "storage", + "tabs", + "activeTab", + "notifications", + "webRequest", + "webRequestBlocking", + "<all_urls>", + "http://*/*", + "https://*/*", + "ftp://*/*", + "file:///*" + ], + "platform": "all", + "hash": "sha256:f358b24d0b950f5acf035342dec64c99ee2e22a5cf369e7c787ebb00013127a8", + "is_mozilla_signed_extension": false, + "is_webextension": true, + "status": "public", + "url": "https://addons-dev.allizom.org/firefox/downloads/file/262271/search_by_image_reverse_image_search-1.12.6-fx.xpi?src=", + "is_restart_required": false, + "size": 372225, + "created": "2018-12-14T13:48:23Z" + } + ], + "id": 1541610, + "is_strict_compatibility_enabled": false, + "compatibility": { + "firefox": { + "min": "57.0", + "max": "*" + } + } + }, + "average_daily_users": 374 + }, + "description_text": "AAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGG" + }, + { + "addon": { + "icon_url": "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png", + "guid": "{f5e7a6ee-ebe0-4add-8f75-b5e4015feca1}", + "authors": [ + { + "id": 8733220, + "url": "https://addons-dev.allizom.org/en-US/firefox/user/8733220/", + "username": "michellet-2", + "name": "michellet", + "picture_url": null + } + ], + "previews": [ + { + "caption": null, + "thumbnail_size": [473, 64], + "id": 14304, + "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14304.png?modified=1547627480", + "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14304.png?modified=1547627480", + "image_size": [680, 92] + }, + { + "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14305.png?modified=1547627481", + "image_size": [760, 92], + "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14305.png?modified=1547627481", + "id": 14305, + "thumbnail_size": [529, 64], + "caption": null + }, + { + "caption": null, + "thumbnail_size": [501, 64], + "thumbnail_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14306.png?modified=1547627482", + "id": 14306, + "image_size": [720, 92], + "image_url": "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14306.png?modified=1547627482" + } + ], + "name": "Purple Sparkles", + "id": 502858, + "url": "https://addons-dev.allizom.org/en-US/firefox/addon/purple-sparkles/", + "type": "statictheme", + "ratings": { + "count": 4, + "bayesian_average": 4.1476, + "average": 4.25, + "text_count": 3 + }, + "slug": "purple-sparkles", + "average_daily_users": 445, + "current_version": { + "compatibility": { + "firefox": { + "min": "53.0", + "max": "*" + }, + "android": { + "max": "*", + "min": "65.0" + } + }, + "id": 1541671, + "is_strict_compatibility_enabled": false, + "files": [ + { + "created": "2019-01-16T08:31:18Z", + "size": 237348, + "url": "https://addons-dev.allizom.org/firefox/downloads/file/262332/purple_sparkles-1.0-an+fx.xpi?src=", + "is_restart_required": false, + "is_mozilla_signed_extension": false, + "is_webextension": true, + "status": "public", + "id": 262332, + "hash": "sha256:5a3d311b7c1be2ee32446dbcf1422c5d7c786c5a237aa3d4e2939074ab50ad30", + "platform": "all", + "permissions": [] + } + ] + } + }, + "description_text": "", + "is_recommendation": false + } + ], + "count": 9 +} diff --git a/toolkit/mozapps/extensions/test/browser/discovery/api_response_empty.json b/toolkit/mozapps/extensions/test/browser/discovery/api_response_empty.json new file mode 100644 index 0000000000..a5a3af7835 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/discovery/api_response_empty.json @@ -0,0 +1 @@ +{ "results": [] } diff --git a/toolkit/mozapps/extensions/test/browser/discovery/small-1x1.png b/toolkit/mozapps/extensions/test/browser/discovery/small-1x1.png Binary files differnew file mode 100644 index 0000000000..862d1dd10c --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/discovery/small-1x1.png diff --git a/toolkit/mozapps/extensions/test/browser/head.js b/toolkit/mozapps/extensions/test/browser/head.js new file mode 100644 index 0000000000..482429177c --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/head.js @@ -0,0 +1,1714 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +/* globals end_test */ + +/* eslint no-unused-vars: ["error", {vars: "local", args: "none"}] */ + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +let { AddonManagerPrivate } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); + +var pathParts = gTestPath.split("/"); +// Drop the test filename +pathParts.splice(pathParts.length - 1, pathParts.length); + +const RELATIVE_DIR = pathParts.slice(4).join("/") + "/"; + +const TESTROOT = "http://example.com/" + RELATIVE_DIR; +const SECURE_TESTROOT = "https://example.com/" + RELATIVE_DIR; +const TESTROOT2 = "http://example.org/" + RELATIVE_DIR; +const SECURE_TESTROOT2 = "https://example.org/" + RELATIVE_DIR; +const CHROMEROOT = pathParts.join("/") + "/"; +const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane"; +const PREF_XPI_ENABLED = "xpinstall.enabled"; +const PREF_UPDATEURL = "extensions.update.url"; +const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled"; +const PREF_UI_LASTCATEGORY = "extensions.ui.lastCategory"; + +const MANAGER_URI = "about:addons"; +const PREF_LOGGING_ENABLED = "extensions.logging.enabled"; +const PREF_STRICT_COMPAT = "extensions.strictCompatibility"; + +var PREF_CHECK_COMPATIBILITY; +(function () { + var channel = Services.prefs.getCharPref("app.update.channel", "default"); + if ( + channel != "aurora" && + channel != "beta" && + channel != "release" && + channel != "esr" + ) { + var version = "nightly"; + } else { + version = Services.appinfo.version.replace( + /^([^\.]+\.[0-9]+[a-z]*).*/gi, + "$1" + ); + } + PREF_CHECK_COMPATIBILITY = "extensions.checkCompatibility." + version; +})(); + +var gPendingTests = []; +var gTestsRun = 0; +var gTestStart = null; + +var gRestorePrefs = [ + { name: PREF_LOGGING_ENABLED }, + { name: "extensions.webservice.discoverURL" }, + { name: "extensions.update.url" }, + { name: "extensions.update.background.url" }, + { name: "extensions.update.enabled" }, + { name: "extensions.update.autoUpdateDefault" }, + { name: "extensions.getAddons.get.url" }, + { name: "extensions.getAddons.getWithPerformance.url" }, + { name: "extensions.getAddons.cache.enabled" }, + { name: "devtools.chrome.enabled" }, + { name: PREF_STRICT_COMPAT }, + { name: PREF_CHECK_COMPATIBILITY }, +]; + +for (let pref of gRestorePrefs) { + if (!Services.prefs.prefHasUserValue(pref.name)) { + pref.type = "clear"; + continue; + } + pref.type = Services.prefs.getPrefType(pref.name); + if (pref.type == Services.prefs.PREF_BOOL) { + pref.value = Services.prefs.getBoolPref(pref.name); + } else if (pref.type == Services.prefs.PREF_INT) { + pref.value = Services.prefs.getIntPref(pref.name); + } else if (pref.type == Services.prefs.PREF_STRING) { + pref.value = Services.prefs.getCharPref(pref.name); + } +} + +// Turn logging on for all tests +Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true); + +function promiseFocus(window) { + return new Promise(resolve => waitForFocus(resolve, window)); +} + +// Tools to disable and re-enable the background update and blocklist timers +// so that tests can protect themselves from unwanted timer events. +var gCatMan = Services.catMan; +// Default value from toolkit/mozapps/extensions/extensions.manifest, but disable*UpdateTimer() +// records the actual value so we can put it back in enable*UpdateTimer() +var backgroundUpdateConfig = + "@mozilla.org/addons/integration;1,getService,addon-background-update-timer,extensions.update.interval,86400"; + +var UTIMER = "update-timer"; +var AMANAGER = "addonManager"; +var BLOCKLIST = "nsBlocklistService"; + +function disableBackgroundUpdateTimer() { + info("Disabling " + UTIMER + " " + AMANAGER); + backgroundUpdateConfig = gCatMan.getCategoryEntry(UTIMER, AMANAGER); + gCatMan.deleteCategoryEntry(UTIMER, AMANAGER, true); +} + +function enableBackgroundUpdateTimer() { + info("Enabling " + UTIMER + " " + AMANAGER); + gCatMan.addCategoryEntry( + UTIMER, + AMANAGER, + backgroundUpdateConfig, + false, + true + ); +} + +registerCleanupFunction(function () { + // Restore prefs + for (let pref of gRestorePrefs) { + if (pref.type == "clear") { + Services.prefs.clearUserPref(pref.name); + } else if (pref.type == Services.prefs.PREF_BOOL) { + Services.prefs.setBoolPref(pref.name, pref.value); + } else if (pref.type == Services.prefs.PREF_INT) { + Services.prefs.setIntPref(pref.name, pref.value); + } else if (pref.type == Services.prefs.PREF_STRING) { + Services.prefs.setCharPref(pref.name, pref.value); + } + } + + return AddonManager.getAllInstalls().then(aInstalls => { + for (let install of aInstalls) { + if (install instanceof MockInstall) { + continue; + } + + ok( + false, + "Should not have seen an install of " + + install.sourceURI.spec + + " in state " + + install.state + ); + install.cancel(); + } + }); +}); + +function log_exceptions(aCallback, ...aArgs) { + try { + return aCallback.apply(null, aArgs); + } catch (e) { + info("Exception thrown: " + e); + throw e; + } +} + +function log_callback(aPromise, aCallback) { + aPromise.then(aCallback).catch(e => info("Exception thrown: " + e)); + return aPromise; +} + +function add_test(test) { + gPendingTests.push(test); +} + +function run_next_test() { + // Make sure we're not calling run_next_test from inside an add_task() test + // We're inside the browser_test.js 'testScope' here + if (this.__tasks) { + throw new Error( + "run_next_test() called from an add_task() test function. " + + "run_next_test() should not be called from inside add_task() " + + "under any circumstances!" + ); + } + if (gTestsRun > 0) { + info("Test " + gTestsRun + " took " + (Date.now() - gTestStart) + "ms"); + } + + if (!gPendingTests.length) { + executeSoon(end_test); + return; + } + + gTestsRun++; + var test = gPendingTests.shift(); + if (test.name) { + info("Running test " + gTestsRun + " (" + test.name + ")"); + } else { + info("Running test " + gTestsRun); + } + + gTestStart = Date.now(); + executeSoon(() => log_exceptions(test)); +} + +var get_tooltip_info = async function (addonEl, managerWindow) { + // Extract from title attribute. + const { addon } = addonEl; + const name = addon.name; + + let nameWithVersion = addonEl.addonNameEl.title; + if (addonEl.addon.userDisabled) { + // TODO - Bug 1558077: Currently Fluent is clearing the addon title + // when the addon is disabled, fixing it requires changes to the + // HTML about:addons localized strings, and then remove this + // workaround. + nameWithVersion = `${name} ${addon.version}`; + } + + return { + name, + version: nameWithVersion.substring(name.length + 1), + }; +}; + +function get_addon_file_url(aFilename) { + try { + var cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService( + Ci.nsIChromeRegistry + ); + var fileurl = cr.convertChromeURL( + makeURI(CHROMEROOT + "addons/" + aFilename) + ); + return fileurl.QueryInterface(Ci.nsIFileURL); + } catch (ex) { + var jar = getJar(CHROMEROOT + "addons/" + aFilename); + var tmpDir = extractJarToTmp(jar); + tmpDir.append(aFilename); + + return Services.io.newFileURI(tmpDir).QueryInterface(Ci.nsIFileURL); + } +} + +function check_all_in_list(aManager, aIds, aIgnoreExtras) { + var doc = aManager.document; + var list = doc.getElementById("addon-list"); + + var inlist = []; + var node = list.firstChild; + while (node) { + if (node.value) { + inlist.push(node.value); + } + node = node.nextSibling; + } + + for (let id of aIds) { + if (!inlist.includes(id)) { + ok(false, "Should find " + id + " in the list"); + } + } + + if (aIgnoreExtras) { + return; + } + + for (let inlistItem of inlist) { + if (!aIds.includes(inlistItem)) { + ok(false, "Shouldn't have seen " + inlistItem + " in the list"); + } + } +} + +function getAddonCard(win, id) { + return win.document.querySelector(`addon-card[addon-id="${id}"]`); +} + +async function wait_for_view_load( + aManagerWindow, + aCallback, + aForceWait, + aLongerTimeout +) { + // Wait one tick to make sure that the microtask related to an + // async loadView call originated from outsite about:addons + // is already executing (otherwise isLoading would be still false + // and we wouldn't be waiting for that load before resolving + // the promise returned by this test helper function). + await Promise.resolve(); + + let p = new Promise(resolve => { + requestLongerTimeout(aLongerTimeout ? aLongerTimeout : 2); + + if (!aForceWait && !aManagerWindow.gViewController.isLoading) { + resolve(aManagerWindow); + return; + } + + aManagerWindow.document.addEventListener( + "view-loaded", + function () { + resolve(aManagerWindow); + }, + { once: true } + ); + }); + + return log_callback(p, aCallback); +} + +function wait_for_manager_load(aManagerWindow, aCallback) { + info("Waiting for initialization"); + return log_callback( + aManagerWindow.promiseInitialized.then(() => aManagerWindow), + aCallback + ); +} + +function open_manager( + aView, + aCallback, + aLoadCallback, + aLongerTimeout, + aWin = window +) { + let p = new Promise((resolve, reject) => { + async function setup_manager(aManagerWindow) { + if (aLoadCallback) { + log_exceptions(aLoadCallback, aManagerWindow); + } + + if (aView) { + aManagerWindow.loadView(aView); + } + + Assert.notEqual( + aManagerWindow, + null, + "Should have an add-ons manager window" + ); + is( + aManagerWindow.location.href, + MANAGER_URI, + "Should be displaying the correct UI" + ); + + await promiseFocus(aManagerWindow); + info("window has focus, waiting for manager load"); + await wait_for_manager_load(aManagerWindow); + info("Manager waiting for view load"); + await wait_for_view_load(aManagerWindow, null, null, aLongerTimeout); + resolve(aManagerWindow); + } + + info("Loading manager window in tab"); + Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + Services.obs.removeObserver(observer, aTopic); + if (aSubject.location.href != MANAGER_URI) { + info("Ignoring load event for " + aSubject.location.href); + return; + } + setup_manager(aSubject); + }, "EM-loaded"); + + aWin.gBrowser.selectedTab = BrowserTestUtils.addTab(aWin.gBrowser); + aWin.switchToTabHavingURI(MANAGER_URI, true, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + }); + + // The promise resolves with the manager window, so it is passed to the callback + return log_callback(p, aCallback); +} + +function close_manager(aManagerWindow, aCallback, aLongerTimeout) { + let p = new Promise((resolve, reject) => { + requestLongerTimeout(aLongerTimeout ? aLongerTimeout : 2); + + Assert.notEqual( + aManagerWindow, + null, + "Should have an add-ons manager window to close" + ); + is( + aManagerWindow.location.href, + MANAGER_URI, + "Should be closing window with correct URI" + ); + + aManagerWindow.addEventListener("unload", function listener() { + try { + dump("Manager window unload handler\n"); + this.removeEventListener("unload", listener); + resolve(); + } catch (e) { + reject(e); + } + }); + }); + + info("Telling manager window to close"); + aManagerWindow.close(); + info("Manager window close() call returned"); + + return log_callback(p, aCallback); +} + +function restart_manager(aManagerWindow, aView, aCallback, aLoadCallback) { + if (!aManagerWindow) { + return open_manager(aView, aCallback, aLoadCallback); + } + + return close_manager(aManagerWindow).then(() => + open_manager(aView, aCallback, aLoadCallback) + ); +} + +function wait_for_window_open(aCallback) { + let p = new Promise(resolve => { + Services.wm.addListener({ + onOpenWindow(aXulWin) { + Services.wm.removeListener(this); + + let domwindow = aXulWin.docShell.domWindow; + domwindow.addEventListener( + "load", + function () { + executeSoon(function () { + resolve(domwindow); + }); + }, + { once: true } + ); + }, + + onCloseWindow(aWindow) {}, + }); + }); + + return log_callback(p, aCallback); +} + +function formatDate(aDate) { + const dtOptions = { year: "numeric", month: "long", day: "numeric" }; + return aDate.toLocaleDateString(undefined, dtOptions); +} + +function is_hidden(aElement) { + var style = aElement.ownerGlobal.getComputedStyle(aElement); + if (style.display == "none") { + return true; + } + if (style.visibility != "visible") { + return true; + } + + // Hiding a parent element will hide all its children + if (aElement.parentNode != aElement.ownerDocument) { + return is_hidden(aElement.parentNode); + } + + return false; +} + +function is_element_visible(aElement, aMsg) { + isnot(aElement, null, "Element should not be null, when checking visibility"); + ok(!is_hidden(aElement), aMsg || aElement + " should be visible"); +} + +function is_element_hidden(aElement, aMsg) { + isnot(aElement, null, "Element should not be null, when checking visibility"); + ok(is_hidden(aElement), aMsg || aElement + " should be hidden"); +} + +function promiseAddonByID(aId) { + return AddonManager.getAddonByID(aId); +} + +function promiseAddonsByIDs(aIDs) { + return AddonManager.getAddonsByIDs(aIDs); +} +/** + * Install an add-on and call a callback when complete. + * + * The callback will receive the Addon for the installed add-on. + */ +async function install_addon(path, cb, pathPrefix = TESTROOT) { + let install = await AddonManager.getInstallForURL(pathPrefix + path); + let p = new Promise((resolve, reject) => { + install.addListener({ + onInstallEnded: () => resolve(install.addon), + }); + + install.install(); + }); + + return log_callback(p, cb); +} + +function CategoryUtilities(aManagerWindow) { + this.window = aManagerWindow; + this.window.addEventListener("unload", () => (this.window = null), { + once: true, + }); +} + +CategoryUtilities.prototype = { + window: null, + + get _categoriesBox() { + return this.window.document.querySelector("categories-box"); + }, + + getSelectedViewId() { + let selectedItem = this._categoriesBox.querySelector("[selected]"); + isnot(selectedItem, null, "A category should be selected"); + return selectedItem.getAttribute("viewid"); + }, + + get selectedCategory() { + isnot( + this.window, + null, + "Should not get selected category when manager window is not loaded" + ); + let viewId = this.getSelectedViewId(); + let view = this.window.gViewController.parseViewId(viewId); + return view.type == "list" ? view.param : view.type; + }, + + get(categoryType) { + isnot( + this.window, + null, + "Should not get category when manager window is not loaded" + ); + + let button = this._categoriesBox.querySelector(`[name="${categoryType}"]`); + if (button) { + return button; + } + + ok(false, "Should have found a category with type " + categoryType); + return null; + }, + + isVisible(categoryButton) { + isnot( + this.window, + null, + "Should not check visible state when manager window is not loaded" + ); + + // There are some tests checking this before the categories have loaded. + if (!categoryButton) { + return false; + } + + if (categoryButton.disabled || categoryButton.hidden) { + return false; + } + + return !is_hidden(categoryButton); + }, + + isTypeVisible(categoryType) { + return this.isVisible(this.get(categoryType)); + }, + + open(categoryButton) { + isnot( + this.window, + null, + "Should not open category when manager window is not loaded" + ); + ok( + this.isVisible(categoryButton), + "Category should be visible if attempting to open it" + ); + + EventUtils.synthesizeMouseAtCenter(categoryButton, {}, this.window); + + // Use wait_for_view_load until all open_manager calls are gone. + return wait_for_view_load(this.window); + }, + + openType(categoryType) { + return this.open(this.get(categoryType)); + }, +}; + +// Returns a promise that will resolve when the certificate error override has been added, or reject +// if there is some failure. +function addCertOverride(host) { + return new Promise((resolve, reject) => { + let req = new XMLHttpRequest(); + req.open("GET", "https://" + host + "/"); + req.onload = reject; + req.onerror = () => { + if (req.channel && req.channel.securityInfo) { + let securityInfo = req.channel.securityInfo; + if (securityInfo.serverCert) { + let cos = Cc["@mozilla.org/security/certoverride;1"].getService( + Ci.nsICertOverrideService + ); + cos.rememberValidityOverride( + host, + -1, + {}, + securityInfo.serverCert, + false + ); + resolve(); + return; + } + } + reject(); + }; + req.send(null); + }); +} + +// Returns a promise that will resolve when the necessary certificate overrides have been added. +function addCertOverrides() { + return Promise.all([ + addCertOverride("nocert.example.com"), + addCertOverride("self-signed.example.com"), + addCertOverride("untrusted.example.com"), + addCertOverride("expired.example.com"), + ]); +} + +/** *** Mock Provider *****/ + +function MockProvider(addonTypes) { + this.addons = []; + this.installs = []; + this.addonTypes = addonTypes ?? ["extension"]; + + var self = this; + registerCleanupFunction(function () { + if (self.started) { + self.unregister(); + } + }); + + this.register(); +} + +MockProvider.prototype = { + addons: null, + installs: null, + addonTypes: null, + started: null, + queryDelayPromise: Promise.resolve(), + + blockQueryResponses() { + this.queryDelayPromise = new Promise(resolve => { + this._unblockQueries = resolve; + }); + }, + + unblockQueryResponses() { + if (this._unblockQueries) { + this._unblockQueries(); + this._unblockQueries = null; + } else { + throw new Error("Queries are not blocked"); + } + }, + + /** *** Utility functions *****/ + + /** + * Register this provider with the AddonManager + */ + register: function MP_register() { + info("Registering mock add-on provider"); + // addonTypes is supposedly the full set of types supported by the provider. + // The current list is not complete (there are tests that mock add-on types + // other than "extension"), but it doesn't affect tests since addonTypes is + // mainly used to determine whether any of the AddonManager's providers + // support a type, and XPIProvider already defines the types of interest. + AddonManagerPrivate.registerProvider(this, this.addonTypes); + }, + + /** + * Unregister this provider with the AddonManager + */ + unregister: function MP_unregister() { + info("Unregistering mock add-on provider"); + AddonManagerPrivate.unregisterProvider(this); + }, + + /** + * Adds an add-on to the list of add-ons that this provider exposes to the + * AddonManager, dispatching appropriate events in the process. + * + * @param aAddon + * The add-on to add + */ + addAddon: function MP_addAddon(aAddon) { + var oldAddons = this.addons.filter(aOldAddon => aOldAddon.id == aAddon.id); + var oldAddon = oldAddons.length ? oldAddons[0] : null; + + this.addons = this.addons.filter(aOldAddon => aOldAddon.id != aAddon.id); + + this.addons.push(aAddon); + aAddon._provider = this; + + if (!this.started) { + return; + } + + let requiresRestart = + (aAddon.operationsRequiringRestart & + AddonManager.OP_NEEDS_RESTART_INSTALL) != + 0; + AddonManagerPrivate.callInstallListeners( + "onExternalInstall", + null, + aAddon, + oldAddon, + requiresRestart + ); + }, + + /** + * Removes an add-on from the list of add-ons that this provider exposes to + * the AddonManager, dispatching the onUninstalled event in the process. + * + * @param aAddon + * The add-on to add + */ + removeAddon: function MP_removeAddon(aAddon) { + var pos = this.addons.indexOf(aAddon); + if (pos == -1) { + ok( + false, + "Tried to remove an add-on that wasn't registered with the mock provider" + ); + return; + } + + this.addons.splice(pos, 1); + + if (!this.started) { + return; + } + + AddonManagerPrivate.callAddonListeners("onUninstalled", aAddon); + }, + + /** + * Adds an add-on install to the list of installs that this provider exposes + * to the AddonManager, dispatching appropriate events in the process. + * + * @param aInstall + * The add-on install to add + */ + addInstall: function MP_addInstall(aInstall) { + this.installs.push(aInstall); + aInstall._provider = this; + + if (!this.started) { + return; + } + + aInstall.callListeners("onNewInstall"); + }, + + removeInstall: function MP_removeInstall(aInstall) { + var pos = this.installs.indexOf(aInstall); + if (pos == -1) { + ok( + false, + "Tried to remove an install that wasn't registered with the mock provider" + ); + return; + } + + this.installs.splice(pos, 1); + }, + + /** + * Creates a set of mock add-on objects and adds them to the list of add-ons + * managed by this provider. + * + * @param aAddonProperties + * An array of objects containing properties describing the add-ons + * @return Array of the new MockAddons + */ + createAddons: function MP_createAddons(aAddonProperties) { + var newAddons = []; + for (let addonProp of aAddonProperties) { + let addon = new MockAddon(addonProp.id); + for (let prop in addonProp) { + if (prop == "id") { + continue; + } + if (prop == "applyBackgroundUpdates") { + addon._applyBackgroundUpdates = addonProp[prop]; + } else if (prop == "appDisabled") { + addon._appDisabled = addonProp[prop]; + } else if (prop == "userDisabled") { + addon.setUserDisabled(addonProp[prop]); + } else { + addon[prop] = addonProp[prop]; + } + } + if (!addon.optionsType && !!addon.optionsURL) { + addon.optionsType = AddonManager.OPTIONS_TYPE_DIALOG; + } + + // Make sure the active state matches the passed in properties + addon.isActive = addon.shouldBeActive; + + this.addAddon(addon); + newAddons.push(addon); + } + + return newAddons; + }, + + /** + * Creates a set of mock add-on install objects and adds them to the list + * of installs managed by this provider. + * + * @param aInstallProperties + * An array of objects containing properties describing the installs + * @return Array of the new MockInstalls + */ + createInstalls: function MP_createInstalls(aInstallProperties) { + var newInstalls = []; + for (let installProp of aInstallProperties) { + let install = new MockInstall( + installProp.name || null, + installProp.type || null, + null + ); + for (let prop in installProp) { + switch (prop) { + case "name": + case "type": + break; + case "sourceURI": + install[prop] = NetUtil.newURI(installProp[prop]); + break; + default: + install[prop] = installProp[prop]; + } + } + this.addInstall(install); + newInstalls.push(install); + } + + return newInstalls; + }, + + /** *** AddonProvider implementation *****/ + + /** + * Called to initialize the provider. + */ + startup: function MP_startup() { + this.started = true; + }, + + /** + * Called when the provider should shutdown. + */ + shutdown: function MP_shutdown() { + this.started = false; + }, + + /** + * Called to get an Addon with a particular ID. + * + * @param aId + * The ID of the add-on to retrieve + */ + async getAddonByID(aId) { + await this.queryDelayPromise; + + for (let addon of this.addons) { + if (addon.id == aId) { + return addon; + } + } + + return null; + }, + + /** + * Called to get Addons of a particular type. + * + * @param aTypes + * An array of types to fetch. Can be null to get all types. + */ + async getAddonsByTypes(aTypes) { + await this.queryDelayPromise; + + var addons = this.addons.filter(function (aAddon) { + if (aTypes && !!aTypes.length && !aTypes.includes(aAddon.type)) { + return false; + } + return true; + }); + return addons; + }, + + /** + * Called to get the current AddonInstalls, optionally restricting by type. + * + * @param aTypes + * An array of types or null to get all types + */ + async getInstallsByTypes(aTypes) { + await this.queryDelayPromise; + + var installs = this.installs.filter(function (aInstall) { + // Appear to have actually removed cancelled installs from the provider + if (aInstall.state == AddonManager.STATE_CANCELLED) { + return false; + } + + if (aTypes && !!aTypes.length && !aTypes.includes(aInstall.type)) { + return false; + } + + return true; + }); + return installs; + }, + + /** + * Called when a new add-on has been enabled when only one add-on of that type + * can be enabled. + * + * @param aId + * The ID of the newly enabled add-on + * @param aType + * The type of the newly enabled add-on + * @param aPendingRestart + * true if the newly enabled add-on will only become enabled after a + * restart + */ + addonChanged: function MP_addonChanged(aId, aType, aPendingRestart) { + // Not implemented + }, + + /** + * Update the appDisabled property for all add-ons. + */ + updateAddonAppDisabledStates: function MP_updateAddonAppDisabledStates() { + // Not needed + }, + + /** + * Called to get an AddonInstall to download and install an add-on from a URL. + * + * @param {string} aUrl + * The URL to be installed + * @param {object} aOptions + * Options for the install + */ + getInstallForURL: function MP_getInstallForURL(aUrl, aOptions) { + // Not yet implemented + }, + + /** + * Called to get an AddonInstall to install an add-on from a local file. + * + * @param aFile + * The file to be installed + */ + getInstallForFile: function MP_getInstallForFile(aFile) { + // Not yet implemented + }, + + /** + * Called to test whether installing add-ons is enabled. + * + * @return true if installing is enabled + */ + isInstallEnabled: function MP_isInstallEnabled() { + return false; + }, + + /** + * Called to test whether this provider supports installing a particular + * mimetype. + * + * @param aMimetype + * The mimetype to check for + * @return true if the mimetype is supported + */ + supportsMimetype: function MP_supportsMimetype(aMimetype) { + return false; + }, + + /** + * Called to test whether installing add-ons from a URI is allowed. + * + * @param aUri + * The URI being installed from + * @return true if installing is allowed + */ + isInstallAllowed: function MP_isInstallAllowed(aUri) { + return false; + }, +}; + +/** *** Mock Addon object for the Mock Provider *****/ + +function MockAddon(aId, aName, aType, aOperationsRequiringRestart) { + // Only set required attributes. + this.id = aId || ""; + this.name = aName || ""; + this.type = aType || "extension"; + this.version = ""; + this.isCompatible = true; + this.providesUpdatesSecurely = true; + this.blocklistState = 0; + this._appDisabled = false; + this._userDisabled = false; + this._applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE; + this.scope = AddonManager.SCOPE_PROFILE; + this.isActive = true; + this.creator = ""; + this.pendingOperations = 0; + this._permissions = + AddonManager.PERM_CAN_UNINSTALL | + AddonManager.PERM_CAN_ENABLE | + AddonManager.PERM_CAN_DISABLE | + AddonManager.PERM_CAN_UPGRADE | + AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS; + this.operationsRequiringRestart = + aOperationsRequiringRestart != undefined + ? aOperationsRequiringRestart + : AddonManager.OP_NEEDS_RESTART_INSTALL | + AddonManager.OP_NEEDS_RESTART_UNINSTALL | + AddonManager.OP_NEEDS_RESTART_ENABLE | + AddonManager.OP_NEEDS_RESTART_DISABLE; +} + +MockAddon.prototype = { + get isCorrectlySigned() { + if (this.signedState === AddonManager.SIGNEDSTATE_NOT_REQUIRED) { + return true; + } + return this.signedState > AddonManager.SIGNEDSTATE_MISSING; + }, + + get shouldBeActive() { + return ( + !this.appDisabled && + !this._userDisabled && + !(this.pendingOperations & AddonManager.PENDING_UNINSTALL) + ); + }, + + get appDisabled() { + return this._appDisabled; + }, + + set appDisabled(val) { + if (val == this._appDisabled) { + return; + } + + AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, [ + "appDisabled", + ]); + + var currentActive = this.shouldBeActive; + this._appDisabled = val; + var newActive = this.shouldBeActive; + this._updateActiveState(currentActive, newActive); + }, + + get userDisabled() { + return this._userDisabled; + }, + + set userDisabled(val) { + throw new Error("No. Bad."); + }, + + setUserDisabled(val) { + if (val == this._userDisabled) { + return; + } + + var currentActive = this.shouldBeActive; + this._userDisabled = val; + var newActive = this.shouldBeActive; + this._updateActiveState(currentActive, newActive); + }, + + async enable() { + await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); + + this.setUserDisabled(false); + }, + async disable() { + await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); + + this.setUserDisabled(true); + }, + + get permissions() { + let permissions = this._permissions; + if (this.appDisabled || !this._userDisabled) { + permissions &= ~AddonManager.PERM_CAN_ENABLE; + } + if (this.appDisabled || this._userDisabled) { + permissions &= ~AddonManager.PERM_CAN_DISABLE; + } + return permissions; + }, + + set permissions(val) { + this._permissions = val; + }, + + get applyBackgroundUpdates() { + return this._applyBackgroundUpdates; + }, + + set applyBackgroundUpdates(val) { + if ( + val != AddonManager.AUTOUPDATE_DEFAULT && + val != AddonManager.AUTOUPDATE_DISABLE && + val != AddonManager.AUTOUPDATE_ENABLE + ) { + ok(false, "addon.applyBackgroundUpdates set to an invalid value: " + val); + } + this._applyBackgroundUpdates = val; + AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, [ + "applyBackgroundUpdates", + ]); + }, + + isCompatibleWith(aAppVersion, aPlatformVersion) { + return true; + }, + + findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) { + // Tests can implement this if they need to + }, + + async getBlocklistURL() { + return this.blocklistURL; + }, + + uninstall(aAlwaysAllowUndo = false) { + if ( + this.operationsRequiringRestart & + AddonManager.OP_NEED_RESTART_UNINSTALL && + this.pendingOperations & AddonManager.PENDING_UNINSTALL + ) { + throw Components.Exception("Add-on is already pending uninstall"); + } + + var needsRestart = + aAlwaysAllowUndo || + !!( + this.operationsRequiringRestart & + AddonManager.OP_NEEDS_RESTART_UNINSTALL + ); + this.pendingOperations |= AddonManager.PENDING_UNINSTALL; + AddonManagerPrivate.callAddonListeners( + "onUninstalling", + this, + needsRestart + ); + if (!needsRestart) { + this.pendingOperations -= AddonManager.PENDING_UNINSTALL; + this._provider.removeAddon(this); + } else if ( + !(this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_DISABLE) + ) { + this.isActive = false; + } + }, + + cancelUninstall() { + if (!(this.pendingOperations & AddonManager.PENDING_UNINSTALL)) { + throw Components.Exception("Add-on is not pending uninstall"); + } + + this.pendingOperations -= AddonManager.PENDING_UNINSTALL; + this.isActive = this.shouldBeActive; + AddonManagerPrivate.callAddonListeners("onOperationCancelled", this); + }, + + markAsSeen() { + this.seen = true; + }, + + _updateActiveState(currentActive, newActive) { + if (currentActive == newActive) { + return; + } + + if (newActive == this.isActive) { + this.pendingOperations -= newActive + ? AddonManager.PENDING_DISABLE + : AddonManager.PENDING_ENABLE; + AddonManagerPrivate.callAddonListeners("onOperationCancelled", this); + } else if (newActive) { + let needsRestart = !!( + this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_ENABLE + ); + this.pendingOperations |= AddonManager.PENDING_ENABLE; + AddonManagerPrivate.callAddonListeners("onEnabling", this, needsRestart); + if (!needsRestart) { + this.isActive = newActive; + this.pendingOperations -= AddonManager.PENDING_ENABLE; + AddonManagerPrivate.callAddonListeners("onEnabled", this); + } + } else { + let needsRestart = !!( + this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_DISABLE + ); + this.pendingOperations |= AddonManager.PENDING_DISABLE; + AddonManagerPrivate.callAddonListeners("onDisabling", this, needsRestart); + if (!needsRestart) { + this.isActive = newActive; + this.pendingOperations -= AddonManager.PENDING_DISABLE; + AddonManagerPrivate.callAddonListeners("onDisabled", this); + } + } + }, +}; + +/** *** Mock AddonInstall object for the Mock Provider *****/ + +function MockInstall(aName, aType, aAddonToInstall) { + this.name = aName || ""; + // Don't expose type until download completed + this._type = aType || "extension"; + this.type = null; + this.version = "1.0"; + this.iconURL = ""; + this.infoURL = ""; + this.state = AddonManager.STATE_AVAILABLE; + this.error = 0; + this.sourceURI = null; + this.file = null; + this.progress = 0; + this.maxProgress = -1; + this.certificate = null; + this.certName = ""; + this.existingAddon = null; + this.addon = null; + this._addonToInstall = aAddonToInstall; + this.listeners = []; + + // Another type of install listener for tests that want to check the results + // of code run from standard install listeners + this.testListeners = []; +} + +MockInstall.prototype = { + install() { + switch (this.state) { + case AddonManager.STATE_AVAILABLE: + this.state = AddonManager.STATE_DOWNLOADING; + if (!this.callListeners("onDownloadStarted")) { + this.state = AddonManager.STATE_CANCELLED; + this.callListeners("onDownloadCancelled"); + return; + } + + this.type = this._type; + + // Adding addon to MockProvider to be implemented when needed + if (this._addonToInstall) { + this.addon = this._addonToInstall; + } else { + this.addon = new MockAddon("", this.name, this.type); + this.addon.version = this.version; + this.addon.pendingOperations = AddonManager.PENDING_INSTALL; + } + this.addon.install = this; + if (this.existingAddon) { + if (!this.addon.id) { + this.addon.id = this.existingAddon.id; + } + this.existingAddon.pendingUpgrade = this.addon; + this.existingAddon.pendingOperations |= AddonManager.PENDING_UPGRADE; + } + + this.state = AddonManager.STATE_DOWNLOADED; + this.callListeners("onDownloadEnded"); + // fall through + case AddonManager.STATE_DOWNLOADED: + this.state = AddonManager.STATE_INSTALLING; + if (!this.callListeners("onInstallStarted")) { + this.state = AddonManager.STATE_CANCELLED; + this.callListeners("onInstallCancelled"); + return; + } + + let needsRestart = + this.operationsRequiringRestart & + AddonManager.OP_NEEDS_RESTART_INSTALL; + AddonManagerPrivate.callAddonListeners( + "onInstalling", + this.addon, + needsRestart + ); + if (!needsRestart) { + AddonManagerPrivate.callAddonListeners("onInstalled", this.addon); + } + + this.state = AddonManager.STATE_INSTALLED; + this.callListeners("onInstallEnded"); + break; + case AddonManager.STATE_DOWNLOADING: + case AddonManager.STATE_CHECKING_UPDATE: + case AddonManager.STATE_INSTALLING: + // Installation is already running + return; + default: + ok(false, "Cannot start installing when state = " + this.state); + } + }, + + cancel() { + switch (this.state) { + case AddonManager.STATE_AVAILABLE: + this.state = AddonManager.STATE_CANCELLED; + break; + case AddonManager.STATE_INSTALLED: + this.state = AddonManager.STATE_CANCELLED; + this._provider.removeInstall(this); + this.callListeners("onInstallCancelled"); + break; + default: + // Handling cancelling when downloading to be implemented when needed + ok(false, "Cannot cancel when state = " + this.state); + } + }, + + addListener(aListener) { + if (!this.listeners.some(i => i == aListener)) { + this.listeners.push(aListener); + } + }, + + removeListener(aListener) { + this.listeners = this.listeners.filter(i => i != aListener); + }, + + addTestListener(aListener) { + if (!this.testListeners.some(i => i == aListener)) { + this.testListeners.push(aListener); + } + }, + + removeTestListener(aListener) { + this.testListeners = this.testListeners.filter(i => i != aListener); + }, + + callListeners(aMethod) { + var result = AddonManagerPrivate.callInstallListeners( + aMethod, + this.listeners, + this, + this.addon + ); + + // Call test listeners after standard listeners to remove race condition + // between standard and test listeners + for (let listener of this.testListeners) { + try { + if (aMethod in listener) { + if (listener[aMethod](this, this.addon) === false) { + result = false; + } + } + } catch (e) { + ok(false, "Test listener threw exception: " + e); + } + } + + return result; + }, +}; + +function waitForCondition(condition, nextTest, errorMsg) { + let tries = 0; + let interval = setInterval(function () { + if (tries >= 30) { + ok(false, errorMsg); + moveOn(); + } + var conditionPassed; + try { + conditionPassed = condition(); + } catch (e) { + ok(false, e + "\n" + e.stack); + conditionPassed = false; + } + if (conditionPassed) { + moveOn(); + } + tries++; + }, 100); + let moveOn = function () { + clearInterval(interval); + nextTest(); + }; +} + +// Wait for and then acknowledge (by pressing the primary button) the +// given notification. +function promiseNotification(id = "addon-webext-permissions") { + return new Promise(resolve => { + function popupshown() { + let notification = PopupNotifications.getNotification(id); + if (notification) { + PopupNotifications.panel.removeEventListener("popupshown", popupshown); + PopupNotifications.panel.firstElementChild.button.click(); + resolve(); + } + } + PopupNotifications.panel.addEventListener("popupshown", popupshown); + }); +} + +/** + * Wait for the given PopupNotification to display + * + * @param {string} name + * The name of the notification to wait for. + * + * @returns {Promise} + * Resolves with the notification window. + */ +function promisePopupNotificationShown(name = "addon-webext-permissions") { + return new Promise(resolve => { + function popupshown() { + let notification = PopupNotifications.getNotification(name); + if (!notification) { + return; + } + + ok(notification, `${name} notification shown`); + ok(PopupNotifications.isPanelOpen, "notification panel open"); + + PopupNotifications.panel.removeEventListener("popupshown", popupshown); + resolve(PopupNotifications.panel.firstChild); + } + PopupNotifications.panel.addEventListener("popupshown", popupshown); + }); +} + +function waitAppMenuNotificationShown( + id, + addonId, + accept = false, + win = window +) { + const { AppMenuNotifications } = ChromeUtils.importESModule( + "resource://gre/modules/AppMenuNotifications.sys.mjs" + ); + return new Promise(resolve => { + let { document, PanelUI } = win; + + async function popupshown() { + let notification = AppMenuNotifications.activeNotification; + if (!notification) { + return; + } + + is(notification.id, id, `${id} notification shown`); + ok(PanelUI.isNotificationPanelOpen, "notification panel open"); + + PanelUI.notificationPanel.removeEventListener("popupshown", popupshown); + + if (id == "addon-installed" && addonId) { + let addon = await AddonManager.getAddonByID(addonId); + if (!addon) { + ok(false, `Addon with id "${addonId}" not found`); + } + let hidden = !( + addon.permissions & + AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS + ); + let checkbox = document.getElementById("addon-incognito-checkbox"); + is(checkbox.hidden, hidden, "checkbox visibility is correct"); + } + if (accept) { + let popupnotificationID = PanelUI._getPopupId(notification); + let popupnotification = document.getElementById(popupnotificationID); + popupnotification.button.click(); + } + + resolve(); + } + // If it's already open just run the test. + let notification = AppMenuNotifications.activeNotification; + if (notification && PanelUI.isNotificationPanelOpen) { + popupshown(); + return; + } + PanelUI.notificationPanel.addEventListener("popupshown", popupshown); + }); +} + +function acceptAppMenuNotificationWhenShown(id, addonId) { + return waitAppMenuNotificationShown(id, addonId, true); +} + +/* HTML view helpers */ +async function loadInitialView(type, opts) { + if (type) { + // Force the first page load to be the view we want. + let viewId; + if (type.startsWith("addons://")) { + viewId = type; + } else { + viewId = + type == "discover" ? "addons://discover/" : `addons://list/${type}`; + } + Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, viewId); + } + + let loadCallback; + let loadCallbackDone = Promise.resolve(); + + if (opts && opts.loadCallback) { + loadCallback = win => { + loadCallbackDone = (async () => { + // Wait for the test code to finish running before proceeding. + await opts.loadCallback(win); + })(); + }; + } + + let win = await open_manager(null, null, loadCallback); + if (!opts || !opts.withAnimations) { + win.document.body.setAttribute("skip-animations", ""); + } + + // Let any load callback code to run before the rest of the test continues. + await loadCallbackDone; + + return win; +} + +function getSection(doc, className) { + return doc.querySelector(`section.${className}`); +} + +function waitForViewLoad(win) { + return wait_for_view_load(win, undefined, true); +} + +function closeView(win) { + return close_manager(win); +} + +function switchView(win, type) { + return new CategoryUtilities(win).openType(type); +} + +function isCategoryVisible(win, type) { + return new CategoryUtilities(win).isTypeVisible(type); +} + +function mockPromptService() { + let { prompt } = Services; + let promptService = { + // The prompt returns 1 for cancelled and 0 for accepted. + _response: 1, + QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]), + confirmEx: () => promptService._response, + }; + Services.prompt = promptService; + registerCleanupFunction(() => { + Services.prompt = prompt; + }); + return promptService; +} + +function assertHasPendingUninstalls(addonList, expectedPendingUninstallsCount) { + const pendingUninstalls = addonList.querySelector( + "message-bar-stack.pending-uninstall" + ); + ok(pendingUninstalls, "Got a pending-uninstall message-bar-stack"); + is( + pendingUninstalls.childElementCount, + expectedPendingUninstallsCount, + "Got a message bar in the pending-uninstall message-bar-stack" + ); +} + +function assertHasPendingUninstallAddon(addonList, addon) { + const pendingUninstalls = addonList.querySelector( + "message-bar-stack.pending-uninstall" + ); + const addonPendingUninstall = addonList.getPendingUninstallBar(addon); + ok( + addonPendingUninstall, + "Got expected message-bar for the pending uninstall test extension" + ); + is( + addonPendingUninstall.parentNode, + pendingUninstalls, + "pending uninstall bar should be part of the message-bar-stack" + ); + is( + addonPendingUninstall.getAttribute("addon-id"), + addon.id, + "Got expected addon-id attribute on the pending uninstall message-bar" + ); +} + +async function testUndoPendingUninstall(addonList, addon) { + const addonPendingUninstall = addonList.getPendingUninstallBar(addon); + const undoButton = addonPendingUninstall.querySelector("button[action=undo]"); + ok(undoButton, "Got undo action button in the pending uninstall message-bar"); + + info( + "Clicking the pending uninstall undo button and wait for addon card rendered" + ); + const updated = BrowserTestUtils.waitForEvent(addonList, "add"); + undoButton.click(); + await updated; + + ok( + addon && !(addon.pendingOperations & AddonManager.PENDING_UNINSTALL), + "The addon pending uninstall cancelled" + ); +} + +function loadTestSubscript(filePath) { + Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this); +} + +function cleanupPendingNotifications() { + const { ExtensionsUI } = ChromeUtils.importESModule( + "resource:///modules/ExtensionsUI.sys.mjs" + ); + info("Cleanup any pending notification before exiting the test"); + const keys = ChromeUtils.nondeterministicGetWeakSetKeys( + ExtensionsUI.pendingNotifications + ); + if (keys) { + keys.forEach(key => ExtensionsUI.pendingNotifications.delete(key)); + } +} + +function promisePermissionPrompt(addonId) { + return BrowserUtils.promiseObserved( + "webextension-permission-prompt", + subject => { + const { info } = subject.wrappedJSObject || {}; + return !addonId || (info.addon && info.addon.id === addonId); + } + ).then(({ subject }) => { + return subject.wrappedJSObject.info; + }); +} + +async function handlePermissionPrompt({ + addonId, + reject = false, + assertIcon = true, +} = {}) { + const info = await promisePermissionPrompt(addonId); + // Assert that info.addon and info.icon are defined as expected. + is( + info.addon && info.addon.id, + addonId, + "Got the AddonWrapper in the permission prompt info" + ); + + if (assertIcon) { + Assert.notEqual( + info.icon, + null, + "Got an addon icon in the permission prompt info" + ); + } + + if (reject) { + info.reject(); + } else { + info.resolve(); + } +} + +async function switchToDetailView({ id, win }) { + let card = getAddonCard(win, id); + ok(card, `Addon card found for ${id}`); + ok(!card.querySelector("addon-details"), "The card doesn't have details"); + let loaded = waitForViewLoad(win); + EventUtils.synthesizeMouseAtCenter( + card.querySelector(".addon-name-link"), + { clickCount: 1 }, + win + ); + await loaded; + card = getAddonCard(win, id); + ok(card.querySelector("addon-details"), "The card does have details"); + return card; +} diff --git a/toolkit/mozapps/extensions/test/browser/head_abuse_report.js b/toolkit/mozapps/extensions/test/browser/head_abuse_report.js new file mode 100644 index 0000000000..f3a683e8d5 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/head_abuse_report.js @@ -0,0 +1,615 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* eslint max-len: ["error", 80] */ + +/* exported installTestExtension, addCommonAbuseReportTestTasks, + * createPromptConfirmEx, DEFAULT_BUILTIN_THEME_ID, + * gManagerWindow, handleSubmitRequest, makeWidgetId, + * waitForNewWindow, waitClosedWindow, AbuseReporter, + * AbuseReporterTestUtils, AddonTestUtils + */ + +/* global MockProvider, loadInitialView, closeView */ + +const { AbuseReporter } = ChromeUtils.importESModule( + "resource://gre/modules/AbuseReporter.sys.mjs" +); +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { ExtensionCommon } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionCommon.sys.mjs" +); + +const { makeWidgetId } = ExtensionCommon; + +const ADDON_ID = "test-extension-to-report@mochi.test"; +const REPORT_ENTRY_POINT = "menu"; +const BASE_TEST_MANIFEST = { + name: "Fake extension to report", + author: "Fake author", + homepage_url: "https://fake.extension.url/", +}; +const DEFAULT_BUILTIN_THEME_ID = "default-theme@mozilla.org"; +const EXT_DICTIONARY_ADDON_ID = "fake-dictionary@mochi.test"; +const EXT_LANGPACK_ADDON_ID = "fake-langpack@mochi.test"; +const EXT_WITH_PRIVILEGED_URL_ID = "ext-with-privileged-url@mochi.test"; +const EXT_SYSTEM_ADDON_ID = "test-system-addon@mochi.test"; +const EXT_UNSUPPORTED_TYPE_ADDON_ID = "report-unsupported-type@mochi.test"; +const THEME_NO_UNINSTALL_ID = "theme-without-perm-can-uninstall@mochi.test"; + +let gManagerWindow; + +AddonTestUtils.initMochitest(this); + +async function openAboutAddons(type = "extension") { + gManagerWindow = await loadInitialView(type); +} + +async function closeAboutAddons() { + if (gManagerWindow) { + await closeView(gManagerWindow); + gManagerWindow = null; + } +} + +function waitForNewWindow() { + return new Promise(resolve => { + let listener = win => { + Services.obs.removeObserver(listener, "toplevel-window-ready"); + resolve(win); + }; + + Services.obs.addObserver(listener, "toplevel-window-ready"); + }); +} + +function waitClosedWindow(win) { + return new Promise((resolve, reject) => { + function onWindowClosed() { + if (win && !win.closed) { + // If a specific window reference has been passed, then check + // that the window is closed before resolving the promise. + return; + } + Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed"); + resolve(); + } + Services.obs.addObserver(onWindowClosed, "xul-window-destroyed"); + }); +} + +async function installTestExtension( + id = ADDON_ID, + type = "extension", + manifest = {} +) { + let additionalProps = { + icons: { + 32: "test-icon.png", + }, + }; + + switch (type) { + case "theme": + additionalProps = { + ...additionalProps, + theme: { + colors: { + frame: "#a14040", + tab_background_text: "#fac96e", + }, + }, + }; + break; + + // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based + // implementation is also removed. + case "sitepermission-deprecated": + additionalProps = { + name: "WebMIDI test addon for https://mochi.test", + install_origins: ["https://mochi.test"], + site_permissions: ["midi"], + }; + break; + case "extension": + break; + default: + throw new Error(`Unexpected addon type: ${type}`); + } + + const extensionOpts = { + manifest: { + ...BASE_TEST_MANIFEST, + ...additionalProps, + ...manifest, + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "temporary", + }; + + // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based + // implementation is also removed. + if (type === "sitepermission-deprecated") { + const xpi = AddonTestUtils.createTempWebExtensionFile(extensionOpts); + const addon = await AddonManager.installTemporaryAddon(xpi); + // The extension object that ExtensionTestUtils.loadExtension returns for + // mochitest is pretty tight to the Extension class, and so for now this + // returns a more minimal `extension` test object which only provides the + // `unload` method. + // + // For the purpose of the abuse reports tests that are using this helper + // this should be already enough. + return { + addon, + unload: () => addon.uninstall(), + }; + } + + const extension = ExtensionTestUtils.loadExtension(extensionOpts); + await extension.startup(); + return extension; +} + +function handleSubmitRequest({ request, response }) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json", false); + response.write("{}"); +} + +const AbuseReportTestUtils = { + _mockProvider: null, + _mockServer: null, + _abuseRequestHandlers: [], + + // Mock addon details API endpoint. + amoAddonDetailsMap: new Map(), + + // Setup the test environment by setting the expected prefs and + // initializing MockProvider and the mock AMO server. + async setup() { + // Enable html about:addons and the abuse reporting and + // set the api endpoints url to the mock service. + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.abuseReport.enabled", true], + ["extensions.abuseReport.url", "http://test.addons.org/api/report/"], + [ + "extensions.abuseReport.amoDetailsURL", + "http://test.addons.org/api/addons/addon/", + ], + ], + }); + + this._setupMockProvider(); + this._setupMockServer(); + }, + + // Returns the currently open abuse report dialog window (if any). + getReportDialog() { + return Services.ww.getWindowByName("addons-abuse-report-dialog"); + }, + + // Returns the parameters related to the report dialog (if any). + getReportDialogParams() { + const win = this.getReportDialog(); + return win && win.arguments[0] && win.arguments[0].wrappedJSObject; + }, + + // Returns a reference to the addon-abuse-report element from the currently + // open abuse report. + getReportPanel() { + const win = this.getReportDialog(); + ok(win, "Got an abuse report dialog open"); + return win && win.document.querySelector("addon-abuse-report"); + }, + + // Returns the list of abuse report reasons. + getReasons(abuseReportEl) { + return Object.keys(abuseReportEl.ownerGlobal.ABUSE_REPORT_REASONS); + }, + + // Returns the info related to a given abuse report reason. + getReasonInfo(abuseReportEl, reason) { + return abuseReportEl.ownerGlobal.ABUSE_REPORT_REASONS[reason]; + }, + + async promiseReportOpened({ addonId, reportEntryPoint, managerWindow }) { + let abuseReportEl; + + if (!this.getReportDialog()) { + info("Wait for the report dialog window"); + const dialog = await waitForNewWindow(); + is(dialog, this.getReportDialog(), "Report dialog opened"); + } + + info("Wait for the abuse report panel render"); + abuseReportEl = await AbuseReportTestUtils.promiseReportDialogRendered(); + + ok(abuseReportEl, "Got an abuse report panel"); + is( + abuseReportEl.addon && abuseReportEl.addon.id, + addonId, + "Abuse Report panel rendered for the expected addonId" + ); + is( + abuseReportEl._report && abuseReportEl._report.reportEntryPoint, + reportEntryPoint, + "Abuse Report panel rendered for the expected reportEntryPoint" + ); + + return abuseReportEl; + }, + + // Return a promise resolved when the currently open report panel + // is closed. + // Also asserts that a specific report panel element has been closed, + // if one has been provided through the optional panel parameter. + async promiseReportClosed(panel) { + const win = panel ? panel.ownerGlobal : this.getReportDialog(); + if (!win || win.closed) { + throw Error("Expected report dialog not found or already closed"); + } + + await waitClosedWindow(win); + // Assert that the panel has been closed (if the caller has passed it). + if (panel) { + ok(!panel.ownerGlobal, "abuse report dialog closed"); + } + }, + + // Returns a promise resolved when the report panel has been rendered + // (rejects is there is no dialog currently open). + async promiseReportDialogRendered() { + const params = this.getReportDialogParams(); + if (!params) { + throw new Error("abuse report dialog not found"); + } + return params.promiseReportPanel; + }, + + // Given a `requestHandler` function, an HTTP server handler function + // to use to handle a report submit request received by the mock AMO server), + // returns a promise resolved when the mock AMO server has received and + // handled the report submit request. + async promiseReportSubmitHandled(requestHandler) { + if (typeof requestHandler !== "function") { + throw new Error("requestHandler should be a function"); + } + return new Promise((resolve, reject) => { + this._abuseRequestHandlers.unshift({ resolve, reject, requestHandler }); + }); + }, + + // Return a promise resolved to the abuse report panel element, + // once its rendering is completed. + // If abuseReportEl is undefined, it looks for the currently opened + // report panel. + async promiseReportRendered(abuseReportEl) { + let el = abuseReportEl; + + if (!el) { + const win = this.getReportDialog(); + if (!win) { + await waitForNewWindow(); + } + + el = await this.promiseReportDialogRendered(); + ok(el, "Got an abuse report panel"); + } + + return el._radioCheckedReason + ? el + : BrowserTestUtils.waitForEvent( + el, + "abuse-report:updated", + "Wait the abuse report panel to be rendered" + ).then(() => el); + }, + + // A promise resolved when the given abuse report panel element + // has been rendered. If a panel name ("reasons" or "submit") is + // passed as a second parameter, it also asserts that the panel is + // updated to the expected view mode. + async promiseReportUpdated(abuseReportEl, panel) { + const evt = await BrowserTestUtils.waitForEvent( + abuseReportEl, + "abuse-report:updated", + "Wait abuse report panel update" + ); + + if (panel) { + is(evt.detail.panel, panel, `Got a "${panel}" update event`); + + const el = abuseReportEl; + switch (evt.detail.panel) { + case "reasons": + ok(!el._reasonsPanel.hidden, "Reasons panel should be visible"); + ok(el._submitPanel.hidden, "Submit panel should be hidden"); + break; + case "submit": + ok(el._reasonsPanel.hidden, "Reasons panel should be hidden"); + ok(!el._submitPanel.hidden, "Submit panel should be visible"); + break; + } + } + }, + + // Returns a promise resolved once the expected number of abuse report + // message bars have been created. + promiseMessageBars(expectedMessageBarCount) { + return new Promise(resolve => { + const details = []; + function listener(evt) { + details.push(evt.detail); + if (details.length >= expectedMessageBarCount) { + cleanup(); + resolve(details); + } + } + function cleanup() { + if (gManagerWindow) { + gManagerWindow.document.removeEventListener( + "abuse-report:new-message-bar", + listener + ); + } + } + gManagerWindow.document.addEventListener( + "abuse-report:new-message-bar", + listener + ); + }); + }, + + async assertFluentStrings(containerEl) { + // Make sure all localized elements have defined Fluent strings. + let localizedEls = Array.from( + containerEl.querySelectorAll("[data-l10n-id]") + ); + if (containerEl.getAttribute("data-l10n-id")) { + localizedEls.push(containerEl); + } + ok(localizedEls.length, "Got localized elements"); + for (let el of localizedEls) { + const l10nId = el.getAttribute("data-l10n-id"); + const l10nAttrs = el.getAttribute("data-l10n-attrs"); + if (!l10nAttrs) { + await TestUtils.waitForCondition( + () => el.textContent !== "", + `Element with Fluent id '${l10nId}' should not be empty` + ); + } else { + await TestUtils.waitForCondition( + () => el.message !== "", + `Message attribute of the element with Fluent id '${l10nId}' + should not be empty` + ); + } + } + }, + + // Assert that the report action visibility on the addon card + // for the given about:addons windows and extension id. + async assertReportActionVisibility(gManagerWindow, extId, expectShown) { + let addonCard = gManagerWindow.document.querySelector( + `addon-list addon-card[addon-id="${extId}"]` + ); + ok(addonCard, `Got the addon-card for the ${extId} test extension`); + + let reportButton = addonCard.querySelector("[action=report]"); + ok(reportButton, `Got the report action for ${extId}`); + Assert.equal( + reportButton.hidden, + !expectShown, + `${extId} report action should be ${expectShown ? "shown" : "hidden"}` + ); + }, + + // Assert that the report action is hidden on the addon card + // for the given about:addons windows and extension id. + assertReportActionHidden(gManagerWindow, extId) { + return this.assertReportActionVisibility(gManagerWindow, extId, false); + }, + + // Assert that the report action is shown on the addon card + // for the given about:addons windows and extension id. + assertReportActionShown(gManagerWindow, extId) { + return this.assertReportActionVisibility(gManagerWindow, extId, true); + }, + + // Assert that the report panel is hidden (or closed if the report + // panel is opened in its own dialog window). + async assertReportPanelHidden() { + const win = this.getReportDialog(); + ok(!win, "Abuse Report dialog should be initially hidden"); + }, + + createMockAddons(mockProviderAddons) { + this._mockProvider.createAddons(mockProviderAddons); + }, + + async clickPanelButton(buttonEl, { label = undefined } = {}) { + info(`Clicking the '${buttonEl.textContent.trim() || label}' button`); + // NOTE: ideally this should synthesize the mouse event, + // we call the click method to prevent intermittent timeouts + // due to the mouse event not received by the target element. + buttonEl.click(); + }, + + triggerNewReport(addonId, reportEntryPoint) { + gManagerWindow.openAbuseReport({ addonId, reportEntryPoint }); + }, + + triggerSubmit(reason, message) { + const reportEl = + this.getReportDialog().document.querySelector("addon-abuse-report"); + reportEl._form.elements.message.value = message; + reportEl._form.elements.reason.value = reason; + reportEl.submit(); + }, + + async openReport(addonId, reportEntryPoint = REPORT_ENTRY_POINT) { + // Close the current about:addons window if it has been leaved open from + // a previous test case failure. + if (gManagerWindow) { + await closeAboutAddons(); + } + + await openAboutAddons(); + + let promiseReportPanel = waitForNewWindow().then(() => + this.promiseReportDialogRendered() + ); + + this.triggerNewReport(addonId, reportEntryPoint); + + const panelEl = await promiseReportPanel; + await this.promiseReportRendered(panelEl); + is(panelEl.addonId, addonId, `Got Abuse Report panel for ${addonId}`); + + return panelEl; + }, + + async closeReportPanel(panelEl) { + const onceReportClosed = AbuseReportTestUtils.promiseReportClosed(panelEl); + + info("Cancel report and wait the dialog to be closed"); + panelEl.dispatchEvent(new CustomEvent("abuse-report:cancel")); + + await onceReportClosed; + }, + + // Internal helper methods. + + _setupMockProvider() { + this._mockProvider = new MockProvider(); + this._mockProvider.createAddons([ + { + id: THEME_NO_UNINSTALL_ID, + name: "This theme cannot be uninstalled", + version: "1.1", + creator: { name: "Theme creator", url: "http://example.com/creator" }, + type: "theme", + permissions: 0, + }, + { + id: EXT_WITH_PRIVILEGED_URL_ID, + name: "This extension has an unexpected privileged creator URL", + version: "1.1", + creator: { name: "creator", url: "about:config" }, + type: "extension", + }, + { + id: EXT_SYSTEM_ADDON_ID, + name: "This is a system addon", + version: "1.1", + creator: { name: "creator", url: "http://example.com/creator" }, + type: "extension", + isSystem: true, + }, + { + id: EXT_UNSUPPORTED_TYPE_ADDON_ID, + name: "This is a fake unsupported addon type", + version: "1.1", + type: "unsupported_addon_type", + }, + { + id: EXT_LANGPACK_ADDON_ID, + name: "This is a fake langpack", + version: "1.1", + type: "locale", + }, + { + id: EXT_DICTIONARY_ADDON_ID, + name: "This is a fake dictionary", + version: "1.1", + type: "dictionary", + }, + ]); + }, + + _setupMockServer() { + if (this._mockServer) { + return; + } + + // Init test report api server. + const server = AddonTestUtils.createHttpServer({ + hosts: ["test.addons.org"], + }); + this._mockServer = server; + + server.registerPathHandler("/api/report/", (request, response) => { + const stream = request.bodyInputStream; + const buffer = NetUtil.readInputStream(stream, stream.available()); + const data = new TextDecoder().decode(buffer); + const promisedHandler = this._abuseRequestHandlers.pop(); + if (promisedHandler) { + const { requestHandler, resolve, reject } = promisedHandler; + try { + requestHandler({ data, request, response }); + resolve(); + } catch (err) { + ok(false, `Unexpected requestHandler error ${err} ${err.stack}\n`); + reject(err); + } + } else { + ok(false, `Unexpected request: ${request.path} ${data}`); + } + }); + + server.registerPrefixHandler("/api/addons/addon/", (request, response) => { + const addonId = request.path.split("/").pop(); + if (!this.amoAddonDetailsMap.has(addonId)) { + response.setStatusLine(request.httpVersion, 404, "Not Found"); + response.write(JSON.stringify({ detail: "Not found." })); + } else { + response.setStatusLine(request.httpVersion, 200, "Success"); + response.write(JSON.stringify(this.amoAddonDetailsMap.get(addonId))); + } + }); + server.registerPathHandler( + "/assets/fake-icon-url.png", + (request, response) => { + response.setStatusLine(request.httpVersion, 200, "Success"); + response.write(""); + response.finish(); + } + ); + }, +}; + +function createPromptConfirmEx({ + remove = false, + report = false, + expectCheckboxHidden = false, +} = {}) { + return (...args) => { + const checkboxState = args.pop(); + const checkboxMessage = args.pop(); + is( + checkboxState && checkboxState.value, + false, + "checkboxState should be initially false" + ); + if (expectCheckboxHidden) { + ok( + !checkboxMessage, + "Should not have a checkboxMessage in promptService.confirmEx call" + ); + } else { + ok( + checkboxMessage, + "Got a checkboxMessage in promptService.confirmEx call" + ); + } + + // Report checkbox selected. + checkboxState.value = report; + + // Remove accepted. + return remove ? 0 : 1; + }; +} diff --git a/toolkit/mozapps/extensions/test/browser/head_disco.js b/toolkit/mozapps/extensions/test/browser/head_disco.js new file mode 100644 index 0000000000..64c346f3dd --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/head_disco.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* eslint max-len: ["error", 80] */ + +/* exported DISCOAPI_DEFAULT_FIXTURE, getCardContainer, + getDiscoveryElement, promiseAddonInstall, promiseDiscopaneUpdate, + promiseEvent, promiseObserved, readAPIResponseFixture */ + +/* globals RELATIVE_DIR, promisePopupNotificationShown, + waitAppMenuNotificationShown */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const { + ExtensionUtils: { promiseEvent, promiseObserved }, +} = ChromeUtils.importESModule("resource://gre/modules/ExtensionUtils.sys.mjs"); + +AddonTestUtils.initMochitest(this); + +// The response to the discovery API, as documented at: +// https://addons-server.readthedocs.io/en/latest/topics/api/discovery.html +// +// The tests using this fixure are meant to verify that the discopane works +// with the latest AMO API. +// The following fixure file should be kept in sync with the content of +// latest AMO API response, e.g. from +// +// https://addons.allizom.org/api/v4/discovery/?lang=en-US +// +// The response must contain at least one theme, and one extension. +const DISCOAPI_DEFAULT_FIXTURE = PathUtils.join( + Services.dirsvc.get("CurWorkD", Ci.nsIFile).path, + ...RELATIVE_DIR.split("/"), + "discovery", + "api_response.json" +); + +// Read the content of API_RESPONSE_FILE, and replaces any embedded URLs with +// URLs that point to the `amoServer` test server. +async function readAPIResponseFixture( + amoTestHost, + fixtureFilePath = DISCOAPI_DEFAULT_FIXTURE +) { + let apiText = await IOUtils.readUTF8(fixtureFilePath); + apiText = apiText.replace(/\bhttps?:\/\/[^"]+(?=")/g, url => { + try { + url = new URL(url); + } catch (e) { + // Responses may contain "http://*/*"; ignore it. + return url; + } + // In this test, we only need to distinguish between different file types, + // so just use the file extension as path name for amoServer. + let ext = url.pathname.split(".").pop(); + return `http://${amoTestHost}/${ext}?${url.pathname}${url.search}`; + }); + + return apiText; +} + +// Wait until the current `<discovery-pane>` element has finished loading its +// cards. This can be used after the cards have been loaded. +function promiseDiscopaneUpdate(win) { + let { cardsReady } = getCardContainer(win); + ok(cardsReady, "Discovery cards should have started to initialize"); + return cardsReady; +} + +function getCardContainer(win) { + return getDiscoveryElement(win).querySelector("recommended-addon-list"); +} + +function getDiscoveryElement(win) { + return win.document.querySelector("discovery-pane"); +} + +// A helper that waits until an installation has been requested from `amoServer` +// and proceeds with approving the installation. +async function promiseAddonInstall( + amoServer, + extensionData, + expectedTelemetryInfo = { source: "disco", taarRecommended: false } +) { + let description = extensionData.manifest.description; + let xpiFile = AddonTestUtils.createTempWebExtensionFile(extensionData); + amoServer.registerFile("/xpi", xpiFile); + + let addonId = + extensionData.manifest?.browser_specific_settings?.gecko?.id || + extensionData.manifest?.applications?.gecko?.id; + let installedPromise = waitAppMenuNotificationShown( + "addon-installed", + addonId, + true + ); + + if (!extensionData.manifest.theme) { + info(`${description}: Waiting for permission prompt`); + // Extensions have install prompts. + let panel = await promisePopupNotificationShown("addon-webext-permissions"); + panel.button.click(); + } else { + info(`${description}: Waiting for install prompt`); + let panel = await promisePopupNotificationShown( + "addon-install-confirmation" + ); + panel.button.click(); + } + + info("Waiting for post-install doorhanger"); + await installedPromise; + + let addon = await AddonManager.getAddonByID(addonId); + Assert.deepEqual( + addon.installTelemetryInfo, + expectedTelemetryInfo, + "The installed add-on should have the expected telemetry info" + ); +} diff --git a/toolkit/mozapps/extensions/test/browser/moz.build b/toolkit/mozapps/extensions/test/browser/moz.build new file mode 100644 index 0000000000..4cc6314d0e --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/moz.build @@ -0,0 +1,31 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +BROWSER_CHROME_MANIFESTS += [ + "browser.toml", +] + +addons = [ + "browser_dragdrop1", + "browser_dragdrop2", + "browser_dragdrop_incompat", + "browser_installssl", + "browser_theme", + "options_signed", +] + +output_dir = ( + OBJDIR_FILES._tests.testing.mochitest.browser.toolkit.mozapps.extensions.test.browser.addons +) + +for addon in addons: + for file_type in ["xpi", "zip"]: + indir = "addons/%s" % addon + path = "%s.%s" % (indir, file_type) + + GeneratedFile(path, script="../create_xpi.py", inputs=[indir]) + + output_dir += ["!%s" % path] diff --git a/toolkit/mozapps/extensions/test/browser/redirect.sjs b/toolkit/mozapps/extensions/test/browser/redirect.sjs new file mode 100644 index 0000000000..8f9d1c08af --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/redirect.sjs @@ -0,0 +1,5 @@ +function handleRequest(request, response) { + dump("*** Received redirect for " + request.queryString + "\n"); + response.setStatusLine(request.httpVersion, 301, "Moved Permanently"); + response.setHeader("Location", request.queryString, false); +} diff --git a/toolkit/mozapps/extensions/test/browser/sandboxed.html b/toolkit/mozapps/extensions/test/browser/sandboxed.html new file mode 100644 index 0000000000..219426f0a9 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/sandboxed.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + </head> + <body> + Sandboxed page + </body> +</html> diff --git a/toolkit/mozapps/extensions/test/browser/sandboxed.html^headers^ b/toolkit/mozapps/extensions/test/browser/sandboxed.html^headers^ new file mode 100644 index 0000000000..4705ce9ded --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/sandboxed.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: sandbox allow-scripts; diff --git a/toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html b/toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html new file mode 100644 index 0000000000..383d2a0986 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> + +<html> +<head> + <meta charset="utf-8"> +</head> +<body> +<p id="result"></p> +<script type="text/javascript"> +let events = []; +let resultEl = document.getElementById("result"); +[ "onEnabling", + "onEnabled", + "onDisabling", + "onDisabled", + "onInstalling", + "onInstalled", + "onUninstalling", + "onUninstalled", + "onOperationCancelled", +].forEach(event => { + navigator.mozAddonManager.addEventListener(event, data => { + let obj = {event, id: data.id, needsRestart: data.needsRestart}; + events.push(JSON.stringify(obj)); + resultEl.textContent = events.join("\n"); + }); +}); +</script> +</body> +</html> diff --git a/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html b/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html new file mode 100644 index 0000000000..141f09cc61 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> + +<html> +<head> + <meta charset="utf-8"> +</head> +<body> +<p id="result"></p> +<script type="text/javascript"> +document.getElementById("result").textContent = ("mozAddonManager" in window.navigator); +</script> +</body> +</html> diff --git a/toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xhtml b/toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xhtml new file mode 100644 index 0000000000..6e3ba328ec --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xhtml @@ -0,0 +1,6 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <browser id="frame" disablehistory="true" flex="1" type="content" + src="https://example.com/browser/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html"/> +</window> diff --git a/toolkit/mozapps/extensions/test/browser/webapi_checkframed.html b/toolkit/mozapps/extensions/test/browser/webapi_checkframed.html new file mode 100644 index 0000000000..1467699789 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/webapi_checkframed.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> + +<html> +<body> +<iframe id="frame" height="200" width="200" src="https://example.com/browser/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html"> +</body> +</html> diff --git a/toolkit/mozapps/extensions/test/browser/webapi_checknavigatedwindow.html b/toolkit/mozapps/extensions/test/browser/webapi_checknavigatedwindow.html new file mode 100644 index 0000000000..e1f96a0b0c --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/webapi_checknavigatedwindow.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> + +<html> +<body> +<script type="text/javascript"> +/* exported openWindow, navigate, check */ +var nav, win; + +function openWindow() { + return new Promise(resolve => { + win = window.open(window.location); + + win.addEventListener("load", function listener() { + nav = win.navigator; + resolve(); + }); + }); +} + +function navigate() { + win.location = "http://example.com/"; +} + +function check() { + return "mozAddonManager" in nav; +} +</script> +</body> +</html> diff --git a/toolkit/mozapps/extensions/test/create_xpi.py b/toolkit/mozapps/extensions/test/create_xpi.py new file mode 100644 index 0000000000..fcd6756e44 --- /dev/null +++ b/toolkit/mozapps/extensions/test/create_xpi.py @@ -0,0 +1,21 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from os.path import abspath, relpath + +from mozbuild.action.zip import main as create_zip + + +def main(output, input_dir, *files): + output.close() + + if files: + # The zip builder doesn't handle the files being an absolute path. + in_files = [relpath(file, input_dir) for file in files] + + return create_zip(["-C", input_dir, abspath(output.name)] + in_files) + else: + return create_zip(["-C", input_dir, abspath(output.name), "**"]) diff --git a/toolkit/mozapps/extensions/test/mochitest/chrome.toml b/toolkit/mozapps/extensions/test/mochitest/chrome.toml new file mode 100644 index 0000000000..607667ee57 --- /dev/null +++ b/toolkit/mozapps/extensions/test/mochitest/chrome.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["test_default_theme.html"] diff --git a/toolkit/mozapps/extensions/test/mochitest/file_empty.html b/toolkit/mozapps/extensions/test/mochitest/file_empty.html new file mode 100644 index 0000000000..b6c8a53b41 --- /dev/null +++ b/toolkit/mozapps/extensions/test/mochitest/file_empty.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<html><head></head><body><span id="text">Nothing to see here</span></body></html> diff --git a/toolkit/mozapps/extensions/test/mochitest/mochitest.toml b/toolkit/mozapps/extensions/test/mochitest/mochitest.toml new file mode 100644 index 0000000000..9567d51802 --- /dev/null +++ b/toolkit/mozapps/extensions/test/mochitest/mochitest.toml @@ -0,0 +1,6 @@ +[DEFAULT] +support-files = ["file_empty.html"] + +["test_blocklist_gfx_initialized.html"] + +["test_bug887098.html"] diff --git a/toolkit/mozapps/extensions/test/mochitest/test_blocklist_gfx_initialized.html b/toolkit/mozapps/extensions/test/mochitest/test_blocklist_gfx_initialized.html new file mode 100644 index 0000000000..3800df3f69 --- /dev/null +++ b/toolkit/mozapps/extensions/test/mochitest/test_blocklist_gfx_initialized.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test gfx blocklist is initialized</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + +add_task(async function check_GfxBlocklistRS_initialized() { + // We have extensive xpcshell tests to ensure the blocklist works + // correctly. Here we just want to ensure the blocklist is indeed + // initialized in a regular browser setup. + // In fact, calling GfxBlocklistRS.checkForEntries() in order to test + // specific functionality would lazily initialize the blocklist, negating + // the value of this test. + let initialized = await SpecialPowers.spawnChrome([], async () => { + const { BlocklistPrivate } = ChromeUtils.importESModule( + "resource://gre/modules/Blocklist.sys.mjs" + ); + + return BlocklistPrivate.GfxBlocklistRS._initialized; + }); + + ok(initialized, "Gfx Blocklist was initialized") +}); + </script> +</head> +<body> +</body> +</html> diff --git a/toolkit/mozapps/extensions/test/mochitest/test_bug887098.html b/toolkit/mozapps/extensions/test/mochitest/test_bug887098.html new file mode 100644 index 0000000000..acf646bd3c --- /dev/null +++ b/toolkit/mozapps/extensions/test/mochitest/test_bug887098.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=887098 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 887098</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + /* exported loaded */ + + /** Test for Bug 887098 **/ + SimpleTest.waitForExplicitFinish(); + /* globals $,evalRef */ + + async function loaded() { + if (!SpecialPowers.Services.prefs.getBoolPref("extensions.InstallTrigger.enabled") || + !SpecialPowers.Services.prefs.getBoolPref("extensions.InstallTriggerImpl.enabled")) { + ok(true, "InstallTrigger is not enabled"); + SimpleTest.finish(); + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [ + // Bug 1703215: Using SpecialPowers causes about:mozilla to be loaded in the wrong + // process, hence we have to flip the pref and don't enforce IPC based Principal Vetting. + ["dom.security.enforceIPCBasedPrincipalVetting", false], + // Relax the user input requirements while running this test. + ["xpinstall.userActivation.required", false], + ], + }); + + var iwin = $("ifr").contentWindow; + var href = SpecialPowers.wrap(iwin).location.href; + if (/file_empty/.test(href)) { + window.evalRef = iwin.eval; + window.installTriggerRef = iwin.InstallTrigger; // Force lazy instantiation. + // about:mozilla is privileged, so we need to be privileged to load it. + SpecialPowers.wrap(iwin).location.href = "about:mozilla"; + } else { + is(href, "about:mozilla", "Successfully navigated to about:mozilla"); + try { + evalRef('InstallTrigger.install({URL: "chrome://global/skin/global.css"});'); + ok(false, "Should have thrown when trying to install restricted URI from InstallTrigger"); + } catch (e) { + // XXXgijs this test broke because of the switch to webidl. I'm told + // it has to do with compartments and the fact that we eval in "about:mozilla". + // Tracking in bug 1007671 + todo(/permission/.test(e), "We should throw a security exception. Got: " + e); + } + SimpleTest.finish(); + } + } + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=887098">Mozilla Bug 887098</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<iframe onload="loaded();" id="ifr" src="file_empty.html"></iframe> +<pre id="test"> +</pre> +</body> +</html> diff --git a/toolkit/mozapps/extensions/test/mochitest/test_default_theme.html b/toolkit/mozapps/extensions/test/mochitest/test_default_theme.html new file mode 100644 index 0000000000..9b48c7136c --- /dev/null +++ b/toolkit/mozapps/extensions/test/mochitest/test_default_theme.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test for correct installation of default theme</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"> +</head> +<body> + +<script> +"use strict"; + +const {AddonManager} = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); +const {AppConstants} = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +add_task(async function() { + let addon = await AddonManager.getAddonByID("default-theme@mozilla.org"); + + // Dev edition uses a different default theme on desktop. + const expectActive = (!AppConstants.MOZ_DEV_EDITION || + AppConstants.MOZ_BUILD_APP !== "browser"); + + ok(addon != null, "Default theme exists"); + is(addon.type, "theme", "Add-on type is correct"); + is(addon.isActive, expectActive, "Add-on is active?"); + is(addon.hidden, false, "Add-on is not hidden"); +}); + +</script> +</body> +</html> diff --git a/toolkit/mozapps/extensions/test/moz.build b/toolkit/mozapps/extensions/test/moz.build new file mode 100644 index 0000000000..00ecaac1bf --- /dev/null +++ b/toolkit/mozapps/extensions/test/moz.build @@ -0,0 +1,20 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += ["browser"] + +BROWSER_CHROME_MANIFESTS += ["xpinstall/browser.toml"] +MOCHITEST_MANIFESTS += ["mochitest/mochitest.toml"] +MOCHITEST_CHROME_MANIFESTS += ["mochitest/chrome.toml"] + +XPCSHELL_TESTS_MANIFESTS += [ + "xpcshell/rs-blocklist/xpcshell.toml", + "xpcshell/xpcshell-unpack.toml", + "xpcshell/xpcshell.toml", +] + +with Files("xpcshell/rs-blocklist/**"): + BUG_COMPONENT = ("Toolkit", "Blocklist Implementation") diff --git a/toolkit/mozapps/extensions/test/xpcshell/.eslintrc.js b/toolkit/mozapps/extensions/test/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..8e3971b385 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/.eslintrc.js @@ -0,0 +1,24 @@ +"use strict"; + +module.exports = { + rules: { + "no-unused-vars": [ + "error", + { args: "none", varsIgnorePattern: "^end_test$" }, + ], + }, + overrides: [ + { + files: "head*.js", + rules: { + "no-unused-vars": [ + "error", + { + args: "none", + vars: "local", + }, + ], + }, + }, + ], +}; diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_block.xml b/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_block.xml new file mode 100644 index 0000000000..1f673ef2fb --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_block.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <emItems> + <emItem id="test_bug455906_1@tests.mozilla.org" blockID="test_bug455906_1@tests.mozilla.org"/> + <emItem id="test_bug455906_2@tests.mozilla.org" blockID="test_bug455906_2@tests.mozilla.org"/> + <emItem id="test_bug455906_3@tests.mozilla.org" blockID="test_bug455906_3@tests.mozilla.org"/> + <emItem id="test_bug455906_4@tests.mozilla.org" blockID="test_bug455906_4@tests.mozilla.org"/> + <emItem id="test_bug455906_5@tests.mozilla.org" blockID="test_bug455906_5@tests.mozilla.org"/> + <emItem id="test_bug455906_6@tests.mozilla.org" blockID="test_bug455906_6@tests.mozilla.org"/> + <emItem id="test_bug455906_7@tests.mozilla.org" blockID="test_bug455906_7@tests.mozilla.org"/> + </emItems> + <pluginItems> + <pluginItem blockID="test_bug455906_plugin"> + <match name="name" exp="^test_bug455906"/> + </pluginItem> + </pluginItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_empty.xml b/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_empty.xml new file mode 100644 index 0000000000..88d22f281f --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_empty.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <emItems> + <emItem id="dummy_bug455906_2@tests.mozilla.org"/> + </emItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_start.xml b/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_start.xml new file mode 100644 index 0000000000..daba6f4c1c --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_start.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <emItems> + <emItem id="test_bug455906_4@tests.mozilla.org"> + <versionRange severity="-1"/> + </emItem> + <emItem id="test_bug455906_5@tests.mozilla.org"> + <versionRange severity="1"/> + </emItem> + <emItem id="test_bug455906_6@tests.mozilla.org"> + <versionRange severity="2"/> + </emItem> + <emItem id="dummy_bug455906_1@tests.mozilla.org"/> + </emItems> + <pluginItems> + <pluginItem> + <match name="name" exp="^test_bug455906_4$"/> + <versionRange severity="0"/> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug455906_5$"/> + <versionRange severity="1"/> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug455906_6$"/> + <versionRange severity="2"/> + </pluginItem> + </pluginItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_warn.xml b/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_warn.xml new file mode 100644 index 0000000000..232fd0d079 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_warn.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <emItems> + <emItem id="test_bug455906_1@tests.mozilla.org"> + <versionRange severity="-1"/> + </emItem> + <emItem id="test_bug455906_2@tests.mozilla.org"> + <versionRange severity="-1"/> + </emItem> + <emItem id="test_bug455906_3@tests.mozilla.org"> + <versionRange severity="-1"/> + </emItem> + <emItem id="test_bug455906_4@tests.mozilla.org"> + <versionRange severity="-1"/> + </emItem> + <emItem id="test_bug455906_5@tests.mozilla.org"> + <versionRange severity="-1"/> + </emItem> + <emItem id="test_bug455906_6@tests.mozilla.org"> + <versionRange severity="-1"/> + </emItem> + <emItem id="test_bug455906_7@tests.mozilla.org"> + <versionRange severity="-1"/> + </emItem> + </emItems> + <pluginItems> + <pluginItem> + <match name="name" exp="^test_bug455906"/> + <versionRange severity="-1"/> + </pluginItem> + </pluginItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/corrupt.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/corrupt.xpi new file mode 100644 index 0000000000..35d7bd5e5d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/corrupt.xpi @@ -0,0 +1 @@ +This is a corrupt zip file diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/corruptfile.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/corruptfile.xpi Binary files differnew file mode 100644 index 0000000000..0c30989aa5 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/corruptfile.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/empty.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/empty.xpi Binary files differnew file mode 100644 index 0000000000..74ed2b8174 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/empty.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/mlbf-blocked1-unblocked2.bin b/toolkit/mozapps/extensions/test/xpcshell/data/mlbf-blocked1-unblocked2.bin Binary files differnew file mode 100644 index 0000000000..fe8e08fa68 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/mlbf-blocked1-unblocked2.bin diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/pluginInfoURL_block.xml b/toolkit/mozapps/extensions/test/xpcshell/data/pluginInfoURL_block.xml new file mode 100644 index 0000000000..75e252a46b --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/pluginInfoURL_block.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <emItems> + </emItems> + <pluginItems> + <pluginItem blockID="test_plugin_wInfoURL"> + <match name="name" exp="^test_with_infoURL"/> + <match name="version" exp="^5"/> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="1" maxVersion="*"/> + </targetApplication> + </versionRange> + <infoURL>http://test.url.com/</infoURL> + </pluginItem> + <pluginItem blockID="test_plugin_wAltInfoURL"> + <match name="name" exp="^test_with_altInfoURL"/> + <match name="version" exp="^5"/> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="1" maxVersion="*"/> + </targetApplication> + </versionRange> + <infoURL>http://alt.test.url.com/</infoURL> + </pluginItem> + <pluginItem blockID="test_plugin_noInfoURL"> + <match name="name" exp="^test_no_infoURL"/> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="1" maxVersion="*"/> + </targetApplication> + </versionRange> + </pluginItem> + <pluginItem blockID="test_plugin_newVersion"> + <match name="name" exp="^test_newVersion"/> + <infoURL>http://test.url2.com/</infoURL> + <versionRange minVersion="1" maxVersion="2"> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="1" maxVersion="*"/> + </targetApplication> + </versionRange> + </pluginItem> + </pluginItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.txt b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.txt new file mode 100644 index 0000000000..f17f98b15b --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.txt @@ -0,0 +1 @@ +Not an xml file!
\ No newline at end of file diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.xml b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.xml new file mode 100644 index 0000000000..0e3d415c44 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<foobar></barfoo>
\ No newline at end of file diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad2.xml b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad2.xml new file mode 100644 index 0000000000..55ad1c7d55 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad2.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<test></test> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_aus_ee.pem b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_aus_ee.pem new file mode 100644 index 0000000000..e7933cc864 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_aus_ee.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICRzCCAS+gAwIBAgIUbctVfUWXUmfxzCRUBeXixFrQuOEwDQYJKoZIhvcNAQEL +BQAwETEPMA0GA1UEAwwGaW50LUNBMCIYDzIwMjIxMTI3MDAwMDAwWhgPMjAyNTAy +MDQwMDAwMDBaMA0xCzAJBgNVBAMMAmVlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE +oWhyQzYrXHsYifN5FUYVocc/tI3uhj4CKRXbYI4lLeS3Ey2ozpjoMVNOapwMCwnI +1jmt6DIG5bqBNHOhH6Mw4F2oyW5Dg/4nhz2pcQO+KIjP8ALwWvcaH93Mg3SqbqnO +o0UwQzATBgNVHSUEDDAKBggrBgEFBQcDAzAsBgNVHREEJTAjgiFhdXMuY29udGVu +dC1zaWduYXR1cmUubW96aWxsYS5vcmcwDQYJKoZIhvcNAQELBQADggEBAF5IT9HZ +1ej+FAXbs2e/LOojJAulc2sxbeaa5V3rJWIiSq8iMj/ZV8dRaa96x3M6azdPiJjD +/VT4mNF9/KBC8YoEwfJe4A9MR8SmEIe/2EMIzmZVdTv1LYsKqRuuwvbGFssBj7lW +U9+V5KzjxtKU/RQfak5Iz+vnl6s4LIt92SLdOooPqDGj2K3FI9dg2Fqwm6vF+6zi +8yZ7/zg4PQcY6t2C6l0e9iAFM+wzhtTPq1AvFdq5hdOil6AS8Ivb0elMwBzsLjr5 +COLcKmCeRQ/8JFhJ48C+/MQkp3gbgXvVR3fcufufSC2YaLmMb7MIhaJwNCT0nO87 +ItpI1owSYrJOnQ0= +-----END CERTIFICATE----- diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_aus_ee.pem.certspec b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_aus_ee.pem.certspec new file mode 100644 index 0000000000..ee9fea9110 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_aus_ee.pem.certspec @@ -0,0 +1,5 @@ +issuer:int-CA +subject:ee +subjectKey:secp384r1 +extension:extKeyUsage:codeSigning +extension:subjectAlternativeName:aus.content-signature.mozilla.org diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_int.pem b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_int.pem new file mode 100644 index 0000000000..6c80b1be43 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_int.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC8TCCAdmgAwIBAgIUGU8IXEaU5Al531xp9aITCfLjy/cwDQYJKoZIhvcNAQEL +BQAwKTEnMCUGA1UEAwweeHBjc2hlbGwgc2lnbmVkIGFwcHMgdGVzdCByb290MCIY +DzIwMjIxMTI3MDAwMDAwWhgPMjAyNTAyMDQwMDAwMDBaMBExDzANBgNVBAMMBmlu +dC1DQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALqIUahEjhbWQf1u +togGNhA9PBPZ6uQ1SrTs9WhXbCR7wcclqODYH72xnAabbhqG8mvir1p1a2pkcQh6 +pVqnRYf3HNUknAJ+zUP8HmnQOCApk6sgw0nk27lMwmtsDu0Vgg/xfq1pGrHTAjqL +KkHup3DgDw2N/WYLK7AkkqR9uYhheZCxV5A90jvF4LhIH6g304hD7ycW2FW3Zlqq +fgKQLzp7EIAGJMwcbJetlmFbt+KWEsB1MaMMkd20yvf8rR0l0wnvuRcOp2jhs3sv +Im9p47SKlWEd7ibWJZ2rkQhONsscJAQsvxaLL+Xxj5kXMbiz/kkj+nJRxDHVA6za +GAo17Y0CAwEAAaMlMCMwDAYDVR0TBAUwAwEB/zATBgNVHSUEDDAKBggrBgEFBQcD +AzANBgkqhkiG9w0BAQsFAAOCAQEAQw8azGUnMeiHd6BYf8LZDK2dqsbVpWuDT/td +LNQcYStX4jgPSfSxm9Mg6osXBnEKF83qXoNeP6Zt84WSJDotEf0WlC5JfNZFCMry +vfd7odumxp/00LYaMbVK8Wz2LXXXwjsYF8xoZz6zq1DYviXIMluhcvCMepnCUnbP +hY12tcznmHiHCOoEB1qurCfW8MkIz/GkLa409i7wFE9rsAeuAKgtdTStY5g8qp5j +2KpmTzgfCeDgKwOSEUyW4YZXrvHYpPSnLiFsWvdxG3/D9aZExw1fipvzhpvqZYv9 +u2e7Qpt98Cd+Kitom/uDNmX9hv6E3eBThQI8QpTf43z6w/KD4A== +-----END CERTIFICATE----- diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_int.pem.certspec b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_int.pem.certspec new file mode 100644 index 0000000000..fc9dfd47ae --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_int.pem.certspec @@ -0,0 +1,4 @@ +issuer:xpcshell signed apps test root +subject:int-CA +extension:basicConstraints:cA, +extension:extKeyUsage:codeSigning diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/empty.xml b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/empty.xml new file mode 100644 index 0000000000..42cb20bd01 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/empty.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<updates> + <addons></addons> +</updates> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/good.xml b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/good.xml new file mode 100644 index 0000000000..e1da86fa54 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/good.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<updates> + <addons> + <addon id="test1" URL="http://example.com/test1.xpi"/> + <addon id="test2" URL="http://example.com/test2.xpi" hashFunction="md5" hashValue="djhfgsjdhf"/> + <addon id="test3" URL="http://example.com/test3.xpi" version="1.0" size="45"/> + <addon id="test4"/> + <addon URL="http://example.com/test5.xpi"/> + </addons> +</updates> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/missing.xml b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/missing.xml new file mode 100644 index 0000000000..8c9501478e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/missing.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<updates></updates> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/unsigned.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/unsigned.xpi Binary files differnew file mode 100644 index 0000000000..51b00475a9 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/unsigned.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/langpack_signed.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/langpack_signed.xpi Binary files differnew file mode 100644 index 0000000000..f60d00348e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/langpack_signed.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/langpack_unsigned.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/langpack_unsigned.xpi Binary files differnew file mode 100644 index 0000000000..89de7f4409 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/langpack_unsigned.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long.xpi Binary files differnew file mode 100644 index 0000000000..f95f3df91e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/privileged.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/privileged.xpi Binary files differnew file mode 100644 index 0000000000..c22acaacd2 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/privileged.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed1.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed1.xpi Binary files differnew file mode 100644 index 0000000000..e2ba7d6fd8 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed1.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed2.xpi Binary files differnew file mode 100644 index 0000000000..ccb20796f2 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed2.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned.xpi Binary files differnew file mode 100644 index 0000000000..9e10be5db3 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_cache.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_cache.json new file mode 100644 index 0000000000..a9fdcf1782 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_cache.json @@ -0,0 +1,134 @@ +{ + "page_size": 25, + "page_count": 1, + "count": 4, + "next": null, + "previous": null, + "results": [ + { + "name": "Repo Add-on 1", + "type": "extension", + "guid": "test_AddonRepository_1@tests.mozilla.org", + "current_version": { + "version": "2.1", + "files": [ + { + "platform": "all", + "size": 9, + "url": "http://example.com/repo/1/install.xpi" + } + ] + }, + "authors": [ + { + "name": "Repo Add-on 1 - Creator", + "url": "http://example.com/repo/1/creator.html" + }, + { + "name": "Repo Add-on 1 - First Developer", + "url": "http://example.com/repo/1/firstDeveloper.html" + }, + { + "name": "Repo Add-on 1 - Second Developer", + "url": "http://example.com/repo/1/secondDeveloper.html" + } + ], + "summary": "Repo Add-on 1 - Description<br>Second line", + "description": "<p>Repo Add-on 1 - Full Description & some extra</p>", + "icons": { + "32": "http://example.com/repo/1/icon.png" + }, + "ratings": { + "count": 1234, + "text_count": 1111, + "average": 1 + }, + "homepage": "http://example.com/repo/1/homepage.html", + "support_url": "http://example.com/repo/1/support.html", + "contributions_url": "http://example.com/repo/1/meetDevelopers.html", + "ratings_url": "http://example.com/repo/1/review.html", + "weekly_downloads": 3331, + "last_updated": "1970-01-01T00:00:09Z" + }, + { + "name": "Repo Add-on 2", + "type": "theme", + "guid": "test_AddonRepository_2@tests.mozilla.org", + "current_version": { + "version": "2.2", + "files": [ + { + "platform": "all", + "size": 9, + "url": "http://example.com/repo/2/install.xpi" + } + ] + }, + "authors": [ + { + "name": "Repo Add-on 2 - Creator", + "url": "http://example.com/repo/2/creator.html" + }, + { + "name": "Repo Add-on 2 - First Developer", + "url": "http://example.com/repo/2/firstDeveloper.html" + }, + { + "name": "Repo Add-on 2 - Second Developer", + "url": "http://example.com/repo/2/secondDeveloper.html" + } + ], + "summary": "Repo Add-on 2 - Description", + "description": "Repo Add-on 2 - Full Description", + "icons": { + "32": "http://example.com/repo/2/icon.png" + }, + "previews": [ + { + "image_url": "http://example.com/repo/2/firstFull.png", + "thumbnail_url": "http://example.com/repo/2/firstThumbnail.png", + "caption": "Repo Add-on 2 - First Caption" + }, + { + "image_url": "http://example.com/repo/2/secondFull.png", + "thumbnail_url": "http://example.com/repo/2/secondThumbnail.png", + "caption": "Repo Add-on 2 - Second Caption" + } + ], + "ratings": { + "count": 2223, + "text_count": 1112, + "average": 2 + }, + "homepage": "http://example.com/repo/2/homepage.html", + "support_url": "http://example.com/repo/2/support.html", + "contributions_url": "http://example.com/repo/2/meetDevelopers.html", + "ratings_url": "http://example.com/repo/2/review.html", + "weekly_downloads": 3332, + "last_updated": "1970-01-01T00:00:09Z" + }, + { + "name": "Repo Add-on 3", + "type": "theme", + "guid": "test_AddonRepository_3@tests.mozilla.org", + "current_version": { + "version": "2.3" + }, + "icons": { + "32": "http://example.com/repo/3/icon.png" + }, + "previews": [ + { + "image_url": "http://example.com/repo/3/firstFull.png", + "thumbnail_url": "http://example.com/repo/3/firstThumbnail.png", + "caption": "Repo Add-on 3 - First Caption" + }, + { + "image_url": "http://example.com/repo/3/secondFull.png", + "thumbnail_url": "http://example.com/repo/3/secondThumbnail.png", + "caption": "Repo Add-on 3 - Second Caption" + } + ] + } + ] +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_empty.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_empty.json new file mode 100644 index 0000000000..c6c09cdf92 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_empty.json @@ -0,0 +1,7 @@ +{ + "page_size": 25, + "count": 0, + "next": null, + "previous": null, + "results": [] +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_fail.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_fail.json new file mode 100644 index 0000000000..d29d525a81 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_fail.json @@ -0,0 +1 @@ +this should yield a json parse error diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getAddonsByIDs.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getAddonsByIDs.json new file mode 100644 index 0000000000..cfd9fcb74a --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getAddonsByIDs.json @@ -0,0 +1,117 @@ +{ + "page_size": 25, + "page_count": 1, + "count": 4, + "next": null, + "previous": null, + "results": [ + { + "name": "PASS", + "type": "extension", + "guid": "test1@tests.mozilla.org", + "current_version": { + "version": "1.1", + "files": [ + { + "platform": "all", + "url": "http://example.com/addons/test_AddonRepository_2.xpi", + "size": 5555 + } + ] + }, + "authors": [ + { + "name": "Test Creator 1", + "url": "http://example.com/creator1.html" + }, + { + "name": "Test Developer 1", + "url": "http://example.com/developer1.html" + } + ], + "summary": "Test Summary 1", + "description": "Test Description 1", + "icons": { + "32": "http://example.com/icon1.png" + }, + "previews": [ + { + "caption": "Caption 1 - 1", + "image_size": [400, 300], + "image_url": "http://example.com/full1-1.png", + "thumbnail_size": [200, 150], + "thumbnail_url": "http://example.com/thumbnail1-1.png" + }, + { + "caption": "Caption 2 - 1", + "image_url": "http://example.com/full2-1.png", + "thumbnail_url": "http://example.com/thumbnail2-1.png" + } + ], + "ratings": { + "count": 1234, + "text_count": 1111, + "average": 4 + }, + "ratings_url": "http://example.com/review1.html", + "support_url": "http://example.com/support1.html", + "contributions_url": "http://example.com/contribution1.html", + "weekly_downloads": 3333, + "last_updated": "2010-02-01T14:04:05Z", + "url": "https://addons.mozilla.org/en-US/firefox/addon/test1@tests.mozilla.org/" + }, + { + "name": "PASS", + "type": "extension", + "guid": "test2@tests.mozilla.org", + "current_version": { + "version": "2.0", + "files": [ + { + "platform": "XPCShell", + "url": "http://example.com/addons/bleah.xpi", + "size": 1000 + } + ] + } + }, + { + "name": "FAIL", + "type": "extension", + "guid": "notRequested@tests.mozilla.org", + "current_version": { + "version": "1.3", + "files": [ + { + "platform": "all", + "url": "http://example.com/test3.xpi" + } + ] + }, + "authors": [ + { + "name": "Test Creator 3" + } + ], + "summary": "Add-on with a guid that wasn't requested should be ignored." + }, + { + "name": "PASS", + "type": "theme", + "guid": "test_AddonRepository_1@tests.mozilla.org", + "current_version": { + "version": "1.4", + "files": [ + { + "platform": "UNKNOWN1", + "url": "http://example.com/test4.xpi" + }, + { + "platform": "UNKNOWN2", + "url": "http://example.com/test4.xpi" + } + ] + } + } + ] +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getMappedAddons.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getMappedAddons.json new file mode 100644 index 0000000000..a392673717 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getMappedAddons.json @@ -0,0 +1,25 @@ +{ + "count": 4, + "next": null, + "page_count": 1, + "page_size": 100, + "previous": null, + "results": [ + { + "addon_guid": "test1@tests.mozilla.org", + "extension_id": "browser-extension-test-1" + }, + { + "addon_guid": "test2@tests.mozilla.org", + "extension_id": "browser-extension-test-2" + }, + { + "addon_guid": "{00000000-1111-2222-3333-444444444444}", + "extension_id": "browser-extension-test-3" + }, + { + "addon_guid": "test_AddonRepository_1@tests.mozilla.org", + "extension_id": "browser-extension-test-4" + } + ] +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getMappedAddons_empty.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getMappedAddons_empty.json new file mode 100644 index 0000000000..add773a29d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getMappedAddons_empty.json @@ -0,0 +1,8 @@ +{ + "count": 0, + "next": null, + "page_count": 1, + "page_size": 100, + "previous": null, + "results": [] +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_backgroundupdate.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_backgroundupdate.json new file mode 100644 index 0000000000..b83e0b04ba --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_backgroundupdate.json @@ -0,0 +1,46 @@ +{ + "addons": { + "addon2@tests.mozilla.org": { + "updates": [ + { + "applications": { + "gecko": { + "strict_min_version": "1", + "advisory_max_version": "1" + } + }, + "version": "2", + "update_link": "http://example.com/broken.xpi" + } + ] + }, + "addon3@tests.mozilla.org": { + "updates": [ + { + "applications": { + "gecko": { + "strict_min_version": "1", + "advisory_max_version": "1" + } + }, + "version": "2", + "update_link": "http://example.com/broken.xpi" + } + ] + }, + "addon1@tests.mozilla.org": { + "updates": [ + { + "applications": { + "gecko": { + "strict_min_version": "1", + "advisory_max_version": "1" + } + }, + "version": "2", + "update_link": "http://example.com/broken.xpi" + } + ] + } + } +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_metadata_filters_1.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_metadata_filters_1.xml new file mode 100644 index 0000000000..b092418bbb --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_metadata_filters_1.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <emItems> + <emItem name="/^Mozilla Corp\.$/"> + <versionRange severity="1"> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="1" maxVersion="2.*"/> + </targetApplication> + </versionRange> + </emItem> + <emItem id="/block2/" name="/^Moz/" + homepageURL="/\.dangerous\.com/" updateURL="/\.dangerous\.com/"> + <versionRange severity="3"> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="1" maxVersion="2.*"/> + </targetApplication> + </versionRange> + </emItem> + </emItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_prefs_1.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_prefs_1.xml new file mode 100644 index 0000000000..41df457b05 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_prefs_1.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <emItems> + <emItem id="block1@tests.mozilla.org"> + <prefs> + <pref>test.blocklist.pref1</pref> + <pref>test.blocklist.pref2</pref> + </prefs> + <versionRange severity="1"> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="1" maxVersion="2.*"/> + </targetApplication> + </versionRange> + </emItem> + <emItem id="block2@tests.mozilla.org"> + <prefs> + <pref>test.blocklist.pref3</pref> + <pref>test.blocklist.pref4</pref> + </prefs> + <versionRange severity="3"> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="1" maxVersion="2.*"/> + </targetApplication> + </versionRange> + </emItem> + </emItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug393285.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug393285.xml new file mode 100644 index 0000000000..1767b4332f --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug393285.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <emItems> + <emItem id="test_bug393285_2@tests.mozilla.org"/> + <emItem id="test_bug393285_3a@tests.mozilla.org"> + <versionRange minVersion="1.0" maxVersion="1.0"/> + </emItem> + <emItem id="test_bug393285_3b@tests.mozilla.org"> + <versionRange minVersion="1.0" maxVersion="1.0"/> + </emItem> + <emItem id="test_bug393285_4@tests.mozilla.org"> + <versionRange minVersion="1.0" maxVersion="1.0"> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="1.0" maxVersion="1.0"/> + </targetApplication> + </versionRange> + </emItem> + <emItem id="test_bug393285_5@tests.mozilla.org" os="Darwin"/> + <emItem id="test_bug393285_6@tests.mozilla.org" os="XPCShell"/> + <emItem id="test_bug393285_7@tests.mozilla.org" os="Darwin,XPCShell,WINNT"/> + <emItem id="test_bug393285_8@tests.mozilla.org" xpcomabi="x86-msvc"/> + <emItem id="test_bug393285_9@tests.mozilla.org" xpcomabi="noarch-spidermonkey"/> + <emItem id="test_bug393285_10@tests.mozilla.org" xpcomabi="ppc-gcc3,noarch-spidermonkey,x86-msvc"/> + <emItem id="test_bug393285_11@tests.mozilla.org" os="Darwin" xpcomabi="ppc-gcc3,x86-msvc"/> + <emItem id="test_bug393285_12@tests.mozilla.org" os="Darwin" xpcomabi="ppc-gcc3,noarch-spidermonkey,x86-msvc"/> + <emItem id="test_bug393285_13@tests.mozilla.org" os="XPCShell" xpcomabi="ppc-gcc3,x86-msvc"/> + <emItem id="test_bug393285_14@tests.mozilla.org" os="XPCShell,WINNT" xpcomabi="ppc-gcc3,x86-msvc,noarch-spidermonkey"/> + </emItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app-extensions.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app-extensions.json new file mode 100644 index 0000000000..2c1fff10c5 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app-extensions.json @@ -0,0 +1,332 @@ +[ + { + "_comment": "Always blocked", + "guid": "test_bug449027_2@tests.mozilla.org", + "versionRange": [] + }, + { + "_comment": "Always blocked", + "guid": "test_bug449027_3@tests.mozilla.org", + "versionRange": [{}] + }, + { + "_comment": "Not blocked since neither version range matches", + "guid": "test_bug449027_4@tests.mozilla.org", + "versionRange": [ + { + "minVersion": "6" + }, + { + "maxVersion": "4" + } + ] + }, + { + "_comment": "Invalid version range, should not block", + "guid": "test_bug449027_5@tests.mozilla.org", + "versionRange": [ + { + "maxVersion": "4", + "minVersion": "6" + } + ] + }, + { + "_comment": "Should block all of these", + "guid": "test_bug449027_6@tests.mozilla.org", + "versionRange": [ + { + "maxVersion": "8", + "minVersion": "7" + }, + { + "maxVersion": "6", + "minVersion": "5" + }, + { + "maxVersion": "4" + } + ] + }, + { + "guid": "test_bug449027_7@tests.mozilla.org", + "versionRange": [ + { + "maxVersion": "4" + }, + { + "maxVersion": "5", + "minVersion": "4" + }, + { + "maxVersion": "7", + "minVersion": "6" + } + ] + }, + { + "guid": "test_bug449027_8@tests.mozilla.org", + "versionRange": [ + { + "maxVersion": "2", + "minVersion": "2" + }, + { + "maxVersion": "6", + "minVersion": "4" + }, + { + "maxVersion": "8", + "minVersion": "7" + } + ] + }, + { + "guid": "test_bug449027_9@tests.mozilla.org", + "versionRange": [ + { + "minVersion": "4" + } + ] + }, + { + "guid": "test_bug449027_10@tests.mozilla.org", + "versionRange": [ + { + "minVersion": "5" + } + ] + }, + { + "guid": "test_bug449027_11@tests.mozilla.org", + "versionRange": [ + { + "maxVersion": "6" + } + ] + }, + { + "guid": "test_bug449027_12@tests.mozilla.org", + "versionRange": [ + { + "maxVersion": "5" + } + ] + }, + { + "_comment": "This should block all versions for any application", + "guid": "test_bug449027_13@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [{}] + } + ] + }, + { + "_comment": "Shouldn't block", + "guid": "test_bug449027_14@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "foo@bar.com" + } + ] + } + ] + }, + { + "_comment": "Should block for any version of the app", + "guid": "test_bug449027_15@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org" + } + ] + } + ] + }, + { + "_comment": "Should still block", + "guid": "test_bug449027_16@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org" + } + ] + } + ] + }, + { + "_comment": "Not blocked since neither version range matches", + "guid": "test_bug449027_17@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org", + "minVersion": "4" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "2" + } + ] + } + ] + }, + { + "_comment": "Invalid version range, should not block", + "guid": "test_bug449027_18@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "4", + "minVersion": "6" + } + ] + } + ] + }, + { + "_comment": "Should block all of these", + "guid": "test_bug449027_19@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "foo@bar.com" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "6", + "minVersion": "5" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "4", + "minVersion": "3" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "2" + } + ] + } + ] + }, + { + "guid": "test_bug449027_20@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "2" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "3", + "minVersion": "2" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "5", + "minVersion": "4" + }, + { + "guid": "foo@bar.com" + } + ] + } + ] + }, + { + "guid": "test_bug449027_21@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "1", + "minVersion": "1" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "4", + "minVersion": "2" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "6", + "minVersion": "5" + } + ] + } + ] + }, + { + "guid": "test_bug449027_22@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "foo@bar.com" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "minVersion": "3" + } + ] + } + ] + }, + { + "guid": "test_bug449027_23@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org", + "minVersion": "2" + }, + { + "guid": "foo@bar.com" + } + ] + } + ] + }, + { + "guid": "test_bug449027_24@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "3" + } + ] + } + ] + }, + { + "guid": "test_bug449027_25@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "4" + } + ] + } + ] + } +] diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app-plugins.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app-plugins.json new file mode 100644 index 0000000000..c88088c9b3 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app-plugins.json @@ -0,0 +1,332 @@ +[ + { + "_comment": "Always blocked", + "matchName": "^test_bug449027_2$", + "versionRange": [] + }, + { + "_comment": "Always blocked", + "matchName": "^test_bug449027_3$", + "versionRange": [{}] + }, + { + "_comment": "Not blocked since neither version range matches", + "matchName": "^test_bug449027_4$", + "versionRange": [ + { + "minVersion": "6" + }, + { + "maxVersion": "4" + } + ] + }, + { + "_comment": "Invalid version range, should not block", + "matchName": "^test_bug449027_5$", + "versionRange": [ + { + "maxVersion": "4", + "minVersion": "6" + } + ] + }, + { + "_comment": "Should block all of these", + "matchName": "^test_bug449027_6$", + "versionRange": [ + { + "maxVersion": "8", + "minVersion": "7" + }, + { + "maxVersion": "6", + "minVersion": "5" + }, + { + "maxVersion": "4" + } + ] + }, + { + "matchName": "^test_bug449027_7$", + "versionRange": [ + { + "maxVersion": "4" + }, + { + "maxVersion": "5", + "minVersion": "4" + }, + { + "maxVersion": "7", + "minVersion": "6" + } + ] + }, + { + "matchName": "^test_bug449027_8$", + "versionRange": [ + { + "maxVersion": "2", + "minVersion": "2" + }, + { + "maxVersion": "6", + "minVersion": "4" + }, + { + "maxVersion": "8", + "minVersion": "7" + } + ] + }, + { + "matchName": "^test_bug449027_9$", + "versionRange": [ + { + "minVersion": "4" + } + ] + }, + { + "matchName": "^test_bug449027_10$", + "versionRange": [ + { + "minVersion": "5" + } + ] + }, + { + "matchName": "^test_bug449027_11$", + "versionRange": [ + { + "maxVersion": "6" + } + ] + }, + { + "matchName": "^test_bug449027_12$", + "versionRange": [ + { + "maxVersion": "5" + } + ] + }, + { + "_comment": "This should block all versions for any application", + "matchName": "^test_bug449027_13$", + "versionRange": [ + { + "targetApplication": [{}] + } + ] + }, + { + "_comment": "Shouldn't block", + "matchName": "^test_bug449027_14$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "foo@bar.com" + } + ] + } + ] + }, + { + "_comment": "Should block for any version of the app", + "matchName": "^test_bug449027_15$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org" + } + ] + } + ] + }, + { + "_comment": "Should still block", + "matchName": "^test_bug449027_16$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org" + } + ] + } + ] + }, + { + "_comment": "Not blocked since neither version range matches", + "matchName": "^test_bug449027_17$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org", + "minVersion": "4" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "2" + } + ] + } + ] + }, + { + "_comment": "Invalid version range, should not block", + "matchName": "^test_bug449027_18$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "4", + "minVersion": "6" + } + ] + } + ] + }, + { + "_comment": "Should block all of these", + "matchName": "^test_bug449027_19$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "foo@bar.com" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "6", + "minVersion": "5" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "4", + "minVersion": "3" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "2" + } + ] + } + ] + }, + { + "matchName": "^test_bug449027_20$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "2" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "3", + "minVersion": "2" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "5", + "minVersion": "4" + }, + { + "guid": "foo@bar.com" + } + ] + } + ] + }, + { + "matchName": "^test_bug449027_21$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "1", + "minVersion": "1" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "4", + "minVersion": "2" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "6", + "minVersion": "5" + } + ] + } + ] + }, + { + "matchName": "^test_bug449027_22$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "foo@bar.com" + }, + { + "guid": "xpcshell@tests.mozilla.org", + "minVersion": "3" + } + ] + } + ] + }, + { + "matchName": "^test_bug449027_23$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org", + "minVersion": "2" + }, + { + "guid": "foo@bar.com" + } + ] + } + ] + }, + { + "matchName": "^test_bug449027_24$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "3" + } + ] + } + ] + }, + { + "matchName": "^test_bug449027_25$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "xpcshell@tests.mozilla.org", + "maxVersion": "4" + } + ] + } + ] + } +] diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app.xml new file mode 100644 index 0000000000..f12ca1fa6d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app.xml @@ -0,0 +1,333 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <emItems> + <!-- All extensions are version 5 and tests run against appVersion 3 --> + + <!-- Test 1 not listed, should never get blocked --> + <!-- Always blocked --> + <emItem id="test_bug449027_2@tests.mozilla.org"/> + <!-- Always blocked --> + <emItem id="test_bug449027_3@tests.mozilla.org"> + <versionRange/> + </emItem> + <!-- Not blocked since neither version range matches --> + <emItem id="test_bug449027_4@tests.mozilla.org"> + <versionRange minVersion="6"/> + <versionRange maxVersion="4"/> + </emItem> + <!-- Invalid version range, should not block --> + <emItem id="test_bug449027_5@tests.mozilla.org"> + <versionRange minVersion="6" maxVersion="4"/> + </emItem> + <!-- Should block all of these --> + <emItem id="test_bug449027_6@tests.mozilla.org"> + <versionRange minVersion="7" maxVersion="8"/> + <versionRange minVersion="5" maxVersion="6"/> + <versionRange maxVersion="4"/> + </emItem> + <emItem id="test_bug449027_7@tests.mozilla.org"> + <versionRange maxVersion="4"/> + <versionRange minVersion="4" maxVersion="5"/> + <versionRange minVersion="6" maxVersion="7"/> + </emItem> + <emItem id="test_bug449027_8@tests.mozilla.org"> + <versionRange minVersion="2" maxVersion="2"/> + <versionRange minVersion="4" maxVersion="6"/> + <versionRange minVersion="7" maxVersion="8"/> + </emItem> + <emItem id="test_bug449027_9@tests.mozilla.org"> + <versionRange minVersion="4"/> + </emItem> + <emItem id="test_bug449027_10@tests.mozilla.org"> + <versionRange minVersion="5"/> + </emItem> + <emItem id="test_bug449027_11@tests.mozilla.org"> + <versionRange maxVersion="6"/> + </emItem> + <emItem id="test_bug449027_12@tests.mozilla.org"> + <versionRange maxVersion="5"/> + </emItem> + + <!-- This should block all versions for any application --> + <emItem id="test_bug449027_13@tests.mozilla.org"> + <versionRange> + <targetApplication/> + </versionRange> + </emItem> + <!-- Shouldn't block --> + <emItem id="test_bug449027_14@tests.mozilla.org"> + <versionRange> + <targetApplication id="foo@bar.com"/> + </versionRange> + </emItem> + <!-- Should block for any version of the app --> + <emItem id="test_bug449027_15@tests.mozilla.org"> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"/> + </versionRange> + </emItem> + <!-- Should still block --> + <emItem id="test_bug449027_16@tests.mozilla.org"> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange/> + </targetApplication> + </versionRange> + </emItem> + <!-- Not blocked since neither version range matches --> + <emItem id="test_bug449027_17@tests.mozilla.org"> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="4"/> + <versionRange maxVersion="2"/> + </targetApplication> + </versionRange> + </emItem> + <!-- Invalid version range, should not block --> + <emItem id="test_bug449027_18@tests.mozilla.org"> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="6" maxVersion="4"/> + </targetApplication> + </versionRange> + </emItem> + <!-- Should block all of these --> + <emItem id="test_bug449027_19@tests.mozilla.org"> + <versionRange> + <targetApplication id="foo@bar.com"/> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="5" maxVersion="6"/> + <versionRange minVersion="3" maxVersion="4"/> + <versionRange maxVersion="2"/> + </targetApplication> + </versionRange> + </emItem> + <emItem id="test_bug449027_20@tests.mozilla.org"> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange maxVersion="2"/> + <versionRange minVersion="2" maxVersion="3"/> + <versionRange minVersion="4" maxVersion="5"/> + </targetApplication> + <targetApplication id="foo@bar.com"/> + </versionRange> + </emItem> + <emItem id="test_bug449027_21@tests.mozilla.org"> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="1" maxVersion="1"/> + <versionRange minVersion="2" maxVersion="4"/> + <versionRange minVersion="5" maxVersion="6"/> + </targetApplication> + </versionRange> + </emItem> + <emItem id="test_bug449027_22@tests.mozilla.org"> + <versionRange> + <targetApplication id="foo@bar.com"/> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="3"/> + </targetApplication> + </versionRange> + </emItem> + <emItem id="test_bug449027_23@tests.mozilla.org"> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="2"/> + </targetApplication> + <targetApplication id="foo@bar.com"/> + </versionRange> + </emItem> + <emItem id="test_bug449027_24@tests.mozilla.org"> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange maxVersion="3"/> + </targetApplication> + </versionRange> + </emItem> + <emItem id="test_bug449027_25@tests.mozilla.org"> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange maxVersion="4"/> + </targetApplication> + </versionRange> + </emItem> + </emItems> + <pluginItems> + <!-- All plugins are version 5 and tests run against appVersion 3 --> + + <!-- Test 1 not listed, should never get blocked --> + <!-- Always blocked --> + <pluginItem> + <match name="name" exp="^test_bug449027_2$"/> + </pluginItem> + <!-- Always blocked --> + <pluginItem> + <match name="name" exp="^test_bug449027_3$"/> + <versionRange/> + </pluginItem> + <!-- Not blocked since neither version range matches --> + <pluginItem> + <match name="name" exp="^test_bug449027_4$"/> + <versionRange minVersion="6"/> + <versionRange maxVersion="4"/> + </pluginItem> + <!-- Invalid version range, should not block --> + <pluginItem> + <match name="name" exp="^test_bug449027_5$"/> + <versionRange minVersion="6" maxVersion="4"/> + </pluginItem> + <!-- Should block all of these --> + <pluginItem> + <match name="name" exp="^test_bug449027_6$"/> + <versionRange minVersion="7" maxVersion="8"/> + <versionRange minVersion="5" maxVersion="6"/> + <versionRange maxVersion="4"/> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_7$"/> + <versionRange maxVersion="4"/> + <versionRange minVersion="4" maxVersion="5"/> + <versionRange minVersion="6" maxVersion="7"/> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_8$"/> + <versionRange minVersion="2" maxVersion="2"/> + <versionRange minVersion="4" maxVersion="6"/> + <versionRange minVersion="7" maxVersion="8"/> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_9$"/> + <versionRange minVersion="4"/> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_10$"/> + <versionRange minVersion="5"/> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_11$"/> + <versionRange maxVersion="6"/> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_12$"/> + <versionRange maxVersion="5"/> + </pluginItem> + + <!-- This should block all versions for any application --> + <pluginItem> + <match name="name" exp="^test_bug449027_13$"/> + <versionRange> + <targetApplication/> + </versionRange> + </pluginItem> + <!-- Shouldn't block --> + <pluginItem> + <match name="name" exp="^test_bug449027_14$"/> + <versionRange> + <targetApplication id="foo@bar.com"/> + </versionRange> + </pluginItem> + <!-- Should block for any version of the app --> + <pluginItem> + <match name="name" exp="^test_bug449027_15$"/> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"/> + </versionRange> + </pluginItem> + <!-- Should still block --> + <pluginItem> + <match name="name" exp="^test_bug449027_16$"/> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange/> + </targetApplication> + </versionRange> + </pluginItem> + <!-- Not blocked since neither version range matches --> + <pluginItem> + <match name="name" exp="^test_bug449027_17$"/> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="4"/> + <versionRange maxVersion="2"/> + </targetApplication> + </versionRange> + </pluginItem> + <!-- Invalid version range, should not block --> + <pluginItem> + <match name="name" exp="^test_bug449027_18$"/> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="6" maxVersion="4"/> + </targetApplication> + </versionRange> + </pluginItem> + <!-- Should block all of these --> + <pluginItem> + <match name="name" exp="^test_bug449027_19$"/> + <versionRange> + <targetApplication id="foo@bar.com"/> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="5" maxVersion="6"/> + <versionRange minVersion="3" maxVersion="4"/> + <versionRange maxVersion="2"/> + </targetApplication> + </versionRange> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_20$"/> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange maxVersion="2"/> + <versionRange minVersion="2" maxVersion="3"/> + <versionRange minVersion="4" maxVersion="5"/> + </targetApplication> + <targetApplication id="foo@bar.com"/> + </versionRange> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_21$"/> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="1" maxVersion="1"/> + <versionRange minVersion="2" maxVersion="4"/> + <versionRange minVersion="5" maxVersion="6"/> + </targetApplication> + </versionRange> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_22$"/> + <versionRange> + <targetApplication id="foo@bar.com"/> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="3"/> + </targetApplication> + </versionRange> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_23$"/> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange minVersion="2"/> + </targetApplication> + <targetApplication id="foo@bar.com"/> + </versionRange> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_24$"/> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange maxVersion="3"/> + </targetApplication> + </versionRange> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_25$"/> + <versionRange> + <targetApplication id="xpcshell@tests.mozilla.org"> + <versionRange maxVersion="4"/> + </targetApplication> + </versionRange> + </pluginItem> + </pluginItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit-extensions.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit-extensions.json new file mode 100644 index 0000000000..107079fd41 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit-extensions.json @@ -0,0 +1,189 @@ +[ + { + "_general_comment": "All extensions are version 5 and tests run against toolkitVersion 8", + "_general_comment2": "Test 1-14 not listed, should never get blocked", + + "_comment": "Should block for any version of the app", + "guid": "test_bug449027_15@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [{ "guid": "toolkit@mozilla.org" }] + } + ] + }, + { + "_comment": "Should still block", + "guid": "test_bug449027_16@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [{ "guid": "toolkit@mozilla.org" }] + } + ] + }, + { + "_comment": "Not blocked since neither version range matches", + "guid": "test_bug449027_17@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "minVersion": "9", + "guid": "toolkit@mozilla.org" + }, + { + "maxVersion": "7", + "guid": "toolkit@mozilla.org" + } + ] + } + ] + }, + { + "_comment": "Invalid version range, should not block", + "guid": "test_bug449027_18@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "minVersion": "11", + "maxVersion": "9", + "guid": "toolkit@mozilla.org" + } + ] + } + ] + }, + { + "_comment": "Should block all of the following", + "guid": "test_bug449027_19@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "foo@bar.com" + }, + { + "guid": "toolkit@mozilla.org", + "minVersion": "10", + "maxVersion": "11" + }, + { + "minVersion": "8", + "maxVersion": "9" + }, + { + "maxVersion": "7" + } + ] + } + ] + }, + { + "guid": "test_bug449027_20@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "toolkit@mozilla.org", + "maxVersion": "7" + }, + { + "guid": "toolkit@mozilla.org", + "minVersion": "7", + "maxVersion": "8" + }, + { + "guid": "toolkit@mozilla.org", + "minVersion": "9", + "maxVersion": "10" + }, + { + "guid": "foo@bar.com" + } + ] + } + ] + }, + { + "guid": "test_bug449027_21@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "toolkit@mozilla.org", + "minVersion": "6", + "maxVersion": "6" + }, + { + "guid": "toolkit@mozilla.org", + "minVersion": "7", + "maxVersion": "9" + }, + { + "guid": "toolkit@mozilla.org", + "minVersion": "10", + "maxVersion": "11" + } + ] + } + ] + }, + { + "guid": "test_bug449027_22@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "foo@bar.com" + }, + { + "guid": "toolkit@mozilla.org", + "minVersion": "8" + } + ] + } + ] + }, + { + "guid": "test_bug449027_23@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "toolkit@mozilla.org", + "minVersion": "7" + }, + { + "guid": "foo@bar.com" + } + ] + } + ] + }, + { + "guid": "test_bug449027_24@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "maxVersion": "8", + "guid": "toolkit@mozilla.org" + } + ] + } + ] + }, + { + "guid": "test_bug449027_25@tests.mozilla.org", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "toolkit@mozilla.org", + "maxVersion": "9" + } + ] + } + ] + } +] diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit-plugins.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit-plugins.json new file mode 100644 index 0000000000..c3565d2073 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit-plugins.json @@ -0,0 +1,189 @@ +[ + { + "_general_comment": "All plugins are version 5 and tests run against appVersion 3", + "_general_comment2": "Test 1-14 not listed, should never get blocked", + + "_comment": "Should block for any version of the app", + "matchName": "^test_bug449027_15$", + "versionRange": [ + { + "targetApplication": [{ "guid": "toolkit@mozilla.org" }] + } + ] + }, + { + "_comment": "Should still block", + "matchName": "^test_bug449027_16$", + "versionRange": [ + { + "targetApplication": [{ "guid": "toolkit@mozilla.org" }] + } + ] + }, + { + "_comment": "Not blocked since neither version range matches", + "matchName": "^test_bug449027_17$", + "versionRange": [ + { + "targetApplication": [ + { + "minVersion": "9", + "guid": "toolkit@mozilla.org" + }, + { + "maxVersion": "7", + "guid": "toolkit@mozilla.org" + } + ] + } + ] + }, + { + "_comment": "Invalid version range, should not block", + "matchName": "^test_bug449027_18$", + "versionRange": [ + { + "targetApplication": [ + { + "minVersion": "11", + "maxVersion": "9", + "guid": "toolkit@mozilla.org" + } + ] + } + ] + }, + { + "_comment": "Should block all of the following", + "matchName": "^test_bug449027_19$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "foo@bar.com" + }, + { + "guid": "toolkit@mozilla.org", + "minVersion": "10", + "maxVersion": "11" + }, + { + "minVersion": "8", + "maxVersion": "9" + }, + { + "maxVersion": "7" + } + ] + } + ] + }, + { + "matchName": "^test_bug449027_20$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "toolkit@mozilla.org", + "maxVersion": "7" + }, + { + "guid": "toolkit@mozilla.org", + "minVersion": "7", + "maxVersion": "8" + }, + { + "guid": "toolkit@mozilla.org", + "minVersion": "9", + "maxVersion": "10" + }, + { + "guid": "foo@bar.com" + } + ] + } + ] + }, + { + "matchName": "^test_bug449027_21$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "toolkit@mozilla.org", + "minVersion": "6", + "maxVersion": "6" + }, + { + "guid": "toolkit@mozilla.org", + "minVersion": "7", + "maxVersion": "9" + }, + { + "guid": "toolkit@mozilla.org", + "minVersion": "10", + "maxVersion": "11" + } + ] + } + ] + }, + { + "matchName": "^test_bug449027_22$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "foo@bar.com" + }, + { + "guid": "toolkit@mozilla.org", + "minVersion": "8" + } + ] + } + ] + }, + { + "matchName": "^test_bug449027_23$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "toolkit@mozilla.org", + "minVersion": "7" + }, + { + "guid": "foo@bar.com" + } + ] + } + ] + }, + { + "matchName": "^test_bug449027_24$", + "versionRange": [ + { + "targetApplication": [ + { + "maxVersion": "8", + "guid": "toolkit@mozilla.org" + } + ] + } + ] + }, + { + "matchName": "^test_bug449027_25$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "toolkit@mozilla.org", + "maxVersion": "9" + } + ] + } + ] + } +] diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit.xml new file mode 100644 index 0000000000..ad8ec5ed9d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit.xml @@ -0,0 +1,208 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <emItems> + <!-- All extensions are version 5 and tests run against toolkitVersion 8 --> + + <!-- Test 1-14 not listed, should never get blocked --> + + <!-- Should block for any version of the app --> + <emItem id="test_bug449027_15@tests.mozilla.org"> + <versionRange> + <targetApplication id="toolkit@mozilla.org"/> + </versionRange> + </emItem> + <!-- Should still block --> + <emItem id="test_bug449027_16@tests.mozilla.org"> + <versionRange> + <targetApplication id="toolkit@mozilla.org"> + <versionRange/> + </targetApplication> + </versionRange> + </emItem> + <!-- Not blocked since neither version range matches --> + <emItem id="test_bug449027_17@tests.mozilla.org"> + <versionRange> + <targetApplication id="toolkit@mozilla.org"> + <versionRange minVersion="9"/> + <versionRange maxVersion="7"/> + </targetApplication> + </versionRange> + </emItem> + <!-- Invalid version range, should not block --> + <emItem id="test_bug449027_18@tests.mozilla.org"> + <versionRange> + <targetApplication id="toolkit@mozilla.org"> + <versionRange minVersion="11" maxVersion="9"/> + </targetApplication> + </versionRange> + </emItem> + <!-- Should block all of these --> + <emItem id="test_bug449027_19@tests.mozilla.org"> + <versionRange> + <targetApplication id="foo@bar.com"/> + <targetApplication id="toolkit@mozilla.org"> + <versionRange minVersion="10" maxVersion="11"/> + <versionRange minVersion="8" maxVersion="9"/> + <versionRange maxVersion="7"/> + </targetApplication> + </versionRange> + </emItem> + <emItem id="test_bug449027_20@tests.mozilla.org"> + <versionRange> + <targetApplication id="toolkit@mozilla.org"> + <versionRange maxVersion="7"/> + <versionRange minVersion="7" maxVersion="8"/> + <versionRange minVersion="9" maxVersion="10"/> + </targetApplication> + <targetApplication id="foo@bar.com"/> + </versionRange> + </emItem> + <emItem id="test_bug449027_21@tests.mozilla.org"> + <versionRange> + <targetApplication id="toolkit@mozilla.org"> + <versionRange minVersion="6" maxVersion="6"/> + <versionRange minVersion="7" maxVersion="9"/> + <versionRange minVersion="10" maxVersion="11"/> + </targetApplication> + </versionRange> + </emItem> + <emItem id="test_bug449027_22@tests.mozilla.org"> + <versionRange> + <targetApplication id="foo@bar.com"/> + <targetApplication id="toolkit@mozilla.org"> + <versionRange minVersion="8"/> + </targetApplication> + </versionRange> + </emItem> + <emItem id="test_bug449027_23@tests.mozilla.org"> + <versionRange> + <targetApplication id="toolkit@mozilla.org"> + <versionRange minVersion="7"/> + </targetApplication> + <targetApplication id="foo@bar.com"/> + </versionRange> + </emItem> + <emItem id="test_bug449027_24@tests.mozilla.org"> + <versionRange> + <targetApplication id="toolkit@mozilla.org"> + <versionRange maxVersion="8"/> + </targetApplication> + </versionRange> + </emItem> + <emItem id="test_bug449027_25@tests.mozilla.org"> + <versionRange> + <targetApplication id="toolkit@mozilla.org"> + <versionRange maxVersion="9"/> + </targetApplication> + </versionRange> + </emItem> + </emItems> + <pluginItems> + <!-- All plugins are version 5 and tests run against appVersion 3 --> + + <!-- Test 1-14 not listed, should never get blocked --> + <!-- Should block for any version of the app --> + <pluginItem> + <match name="name" exp="^test_bug449027_15$"/> + <versionRange> + <targetApplication id="toolkit@mozilla.org"/> + </versionRange> + </pluginItem> + <!-- Should still block --> + <pluginItem> + <match name="name" exp="^test_bug449027_16$"/> + <versionRange> + <targetApplication id="toolkit@mozilla.org"> + <versionRange/> + </targetApplication> + </versionRange> + </pluginItem> + <!-- Not blocked since neither version range matches --> + <pluginItem> + <match name="name" exp="^test_bug449027_17$"/> + <versionRange> + <targetApplication id="toolkit@mozilla.org"> + <versionRange minVersion="9"/> + <versionRange maxVersion="7"/> + </targetApplication> + </versionRange> + </pluginItem> + <!-- Invalid version range, should not block --> + <pluginItem> + <match name="name" exp="^test_bug449027_18$"/> + <versionRange> + <targetApplication id="toolkit@mozilla.org"> + <versionRange minVersion="11" maxVersion="9"/> + </targetApplication> + </versionRange> + </pluginItem> + <!-- Should block all of these --> + <pluginItem> + <match name="name" exp="^test_bug449027_19$"/> + <versionRange> + <targetApplication id="foo@bar.com"/> + <targetApplication id="toolkit@mozilla.org"> + <versionRange minVersion="10" maxVersion="11"/> + <versionRange minVersion="8" maxVersion="9"/> + <versionRange maxVersion="7"/> + </targetApplication> + </versionRange> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_20$"/> + <versionRange> + <targetApplication id="toolkit@mozilla.org"> + <versionRange maxVersion="7"/> + <versionRange minVersion="7" maxVersion="8"/> + <versionRange minVersion="9" maxVersion="10"/> + </targetApplication> + <targetApplication id="foo@bar.com"/> + </versionRange> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_21$"/> + <versionRange> + <targetApplication id="toolkit@mozilla.org"> + <versionRange minVersion="6" maxVersion="6"/> + <versionRange minVersion="7" maxVersion="9"/> + <versionRange minVersion="10" maxVersion="11"/> + </targetApplication> + </versionRange> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_22$"/> + <versionRange> + <targetApplication id="foo@bar.com"/> + <targetApplication id="toolkit@mozilla.org"> + <versionRange minVersion="8"/> + </targetApplication> + </versionRange> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_23$"/> + <versionRange> + <targetApplication id="toolkit@mozilla.org"> + <versionRange minVersion="7"/> + </targetApplication> + <targetApplication id="foo@bar.com"/> + </versionRange> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_24$"/> + <versionRange> + <targetApplication id="toolkit@mozilla.org"> + <versionRange maxVersion="8"/> + </targetApplication> + </versionRange> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug449027_25$"/> + <versionRange> + <targetApplication id="toolkit@mozilla.org"> + <versionRange maxVersion="9"/> + </targetApplication> + </versionRange> + </pluginItem> + </pluginItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug468528.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug468528.xml new file mode 100644 index 0000000000..85f0da57ce --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug468528.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <pluginItems> + <pluginItem> + <match name="name" exp="^test_bug468528_1"/> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug468528_2["/> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug468528_3"/> + </pluginItem> + </pluginItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_1.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_1.xml new file mode 100644 index 0000000000..c4cc2fe37a --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_1.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <pluginItems> + <pluginItem> + <match name="name" exp="^test_bug514327_1"/> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug514327_2"/> + <versionRange severity="0"/> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_bug514327_3"/> + <versionRange severity="0"/> + </pluginItem> + </pluginItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_2.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_2.xml new file mode 100644 index 0000000000..cc0a0c69df --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_2.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <pluginItems> + <pluginItem> + <match name="name" exp="Test Plug-in"/> + <versionRange severity="0"/> + </pluginItem> + </pluginItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_empty.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_empty.xml new file mode 100644 index 0000000000..0261794f8a --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_empty.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_1.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_1.xml new file mode 100644 index 0000000000..d651f87996 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_1.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <pluginItems> + <pluginItem> + <match name="name" exp="test_bug514327_1"/> + </pluginItem> + <pluginItem> + <match name="name" exp="test_bug514327_outdated"/> + <versionRange severity="0"/> + </pluginItem> + </pluginItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_2.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_2.xml new file mode 100644 index 0000000000..208444681e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_2.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <pluginItems> + <pluginItem> + <match name="name" exp="test_bug514327_2"/> + </pluginItem> + <pluginItem> + <match name="name" exp="test_bug514327_outdated"/> + <versionRange severity="0"/> + </pluginItem> + </pluginItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug655254.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug655254.json new file mode 100644 index 0000000000..3b1dd81dab --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug655254.json @@ -0,0 +1,17 @@ +{ + "addons": { + "addon1@tests.mozilla.org": { + "updates": [ + { + "applications": { + "gecko": { + "strict_min_version": "1", + "advisory_max_version": "2" + } + }, + "version": "1" + } + ] + } + } +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_corrupt.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_corrupt.json new file mode 100644 index 0000000000..7cb48d4798 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_corrupt.json @@ -0,0 +1,30 @@ +{ + "addons": { + "addon3@tests.mozilla.org": { + "updates": [ + { + "applications": { + "gecko": { + "strict_min_version": "1", + "advisory_max_version": "2" + } + }, + "version": "1.0" + } + ] + }, + "addon4@tests.mozilla.org": { + "updates": [ + { + "applications": { + "gecko": { + "strict_min_version": "1", + "advisory_max_version": "2" + } + }, + "version": "1.0" + } + ] + } + } +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete.json new file mode 100644 index 0000000000..b79dc236c3 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete.json @@ -0,0 +1,12 @@ +{ + "addons": { + "test_delay_update_complete_webext@tests.mozilla.org": { + "updates": [ + { + "version": "2.0", + "update_link": "http://example.com/addons/test_delay_update_complete_webextension_v2.xpi" + } + ] + } + } +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete_legacy.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete_legacy.json new file mode 100644 index 0000000000..125d1b1a91 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete_legacy.json @@ -0,0 +1,18 @@ +{ + "addons": { + "test_delay_update_complete@tests.mozilla.org": { + "updates": [ + { + "applications": { + "gecko": { + "strict_min_version": "1", + "advisory_max_version": "1" + } + }, + "version": "2.0", + "update_link": "http://example.com/addons/test_delay_update_complete_v2.xpi" + } + ] + } + } +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer.json new file mode 100644 index 0000000000..c2ea01e8c5 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer.json @@ -0,0 +1,12 @@ +{ + "addons": { + "test_delay_update_defer_webext@tests.mozilla.org": { + "updates": [ + { + "version": "2.0", + "update_link": "http://example.com/addons/test_delay_update_defer_webextension_v2.xpi" + } + ] + } + } +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer_legacy.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer_legacy.json new file mode 100644 index 0000000000..d434fe2e17 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer_legacy.json @@ -0,0 +1,18 @@ +{ + "addons": { + "test_delay_update_defer@tests.mozilla.org": { + "updates": [ + { + "applications": { + "gecko": { + "strict_min_version": "1", + "advisory_max_version": "1" + } + }, + "version": "2.0", + "update_link": "http://example.com/addons/test_delay_update_defer_v2.xpi" + } + ] + } + } +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore.json new file mode 100644 index 0000000000..5d5dc262cb --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore.json @@ -0,0 +1,12 @@ +{ + "addons": { + "test_delay_update_ignore_webext@tests.mozilla.org": { + "updates": [ + { + "version": "2.0", + "update_link": "http://example.com/addons/test_delay_update_ignore_webextension_v2.xpi" + } + ] + } + } +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore_legacy.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore_legacy.json new file mode 100644 index 0000000000..bc46fab8fd --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore_legacy.json @@ -0,0 +1,18 @@ +{ + "addons": { + "test_delay_update_ignore@tests.mozilla.org": { + "updates": [ + { + "applications": { + "gecko": { + "strict_min_version": "1", + "advisory_max_version": "1" + } + }, + "version": "2.0", + "update_link": "http://example.com/addons/test_delay_update_ignore_v2.xpi" + } + ] + } + } +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_staged.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_staged.json new file mode 100644 index 0000000000..e0611edb35 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_staged.json @@ -0,0 +1,32 @@ +{ + "addons": { + "test_delay_update_staged_webext@tests.mozilla.org": { + "updates": [ + { + "version": "2.0", + "update_link": "http://example.com/addons/test_delay_update_staged_webextension_v2.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "43" + } + } + } + ] + }, + "test_delay_update_staged_webext_no_update_url@tests.mozilla.org": { + "updates": [ + { + "version": "2.0", + "update_link": "http://example.com/addons/test_delay_update_staged_webextension_no_update_url_v2.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "43" + } + } + } + ] + } + } +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist.json new file mode 100644 index 0000000000..6f5d61288d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist.json @@ -0,0 +1,377 @@ +[ + { + "blockID": "g35", + "os": "WINNT 6.1", + "vendor": "0xabcd", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT2D ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 8.52.322.2202 ", + "driverVersionComparator": " LESS_THAN " + }, + { + "os": "WINNT 6.0", + "vendor": "0xdcba", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_9_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 8.52.322.2202 ", + "driverVersionComparator": " LESS_THAN " + }, + { + "blockID": "g36", + "os": "WINNT 6.1", + "vendor": "0xabab", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT2D ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 8.52.322.2202 ", + "driverVersionComparator": " GREATER_THAN_OR_EQUAL " + }, + { + "os": "WINNT 6.1", + "vendor": "0xdcdc", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT2D ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 8.52.322.1111 ", + "driverVersionComparator": " EQUAL " + }, + { + "os": "Darwin 13", + "vendor": "0xabcd", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT2D ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + { + "os": "Linux", + "vendor": "0xabcd", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT2D ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + { + "os": "Android", + "vendor": "abcd", + "devices": ["wxyz", "asdf", "erty"], + "feature": " DIRECT2D ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 5 ", + "driverVersionComparator": " LESS_THAN_OR_EQUAL " + }, + { + "os": "Android", + "vendor": "dcdc", + "devices": ["uiop", "vbnm", "hjkl"], + "feature": " DIRECT2D ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 5 ", + "driverVersionComparator": " EQUAL " + }, + { + "os": "Android", + "vendor": "abab", + "devices": ["ghjk", "cvbn"], + "feature": " DIRECT2D ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 7 ", + "driverVersionComparator": " GREATER_THAN_OR_EQUAL " + }, + { + "os": "WINNT 6.1", + "vendor": "0xabcd", + "devices": ["0x6666"], + "feature": " DIRECT2D ", + "featureStatus": " BLOCKED_DEVICE " + }, + { + "os": "Darwin 13", + "vendor": "0xabcd", + "devices": ["0x6666"], + "feature": " DIRECT2D ", + "featureStatus": " BLOCKED_DEVICE " + }, + { + "os": "Linux", + "vendor": "0xabcd", + "devices": ["0x6666"], + "feature": " DIRECT2D ", + "featureStatus": " BLOCKED_DEVICE " + }, + { + "os": "Android", + "vendor": "0xabcd", + "devices": ["0x6666"], + "feature": " DIRECT2D ", + "featureStatus": " BLOCKED_DEVICE " + }, + { + "os": "WINNT 6.1", + "vendor": "0xabcd", + "devices": ["0x6666"], + "feature": " WEBRENDER ", + "featureStatus": " BLOCKED_DEVICE " + }, + { + "os": "Darwin 13", + "vendor": "0xabcd", + "devices": ["0x6666"], + "feature": " WEBRENDER ", + "featureStatus": " BLOCKED_DEVICE " + }, + { + "os": "Linux", + "vendor": "0xabcd", + "devices": ["0x6666"], + "feature": " WEBRENDER ", + "featureStatus": " BLOCKED_DEVICE " + }, + { + "os": "Android", + "vendor": "0xabcd", + "devices": ["0x6666"], + "feature": " WEBRENDER ", + "featureStatus": " BLOCKED_DEVICE " + }, + + { + "os": "All", + "vendor": "0xdcdc", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_11_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 8.52.322.1112 ", + "driverVersionMax": " 8.52.323.1000 ", + "driverVersionComparator": " BETWEEN_EXCLUSIVE " + }, + + { + "os": "All", + "vendor": "0xdcdc", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " OPENGL_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 8.50.322.1000 ", + "driverVersionMax": " 8.52.322.1112 ", + "driverVersionComparator": " BETWEEN_EXCLUSIVE " + }, + + { + "os": "All", + "vendor": "0xdcdc", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_11_ANGLE ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 8.52.322.1000 ", + "driverVersionMax": " 9.52.322.1000 ", + "driverVersionComparator": " BETWEEN_EXCLUSIVE " + }, + + { + "os": "All", + "vendor": "0xdcdc", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " HARDWARE_VIDEO_DECODING ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 7.82.322.1000 ", + "driverVersionMax": " 9.25.322.1001 ", + "driverVersionComparator": " BETWEEN_INCLUSIVE " + }, + + { + "os": "All", + "vendor": "0xdcdc", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBRTC_HW_ACCELERATION_H264 ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 8.52.322.1112 ", + "driverVersionMax": " 9.52.322.1300 ", + "driverVersionComparator": " BETWEEN_INCLUSIVE " + }, + + { + "os": "All", + "vendor": "0xdcdc", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBRTC_HW_ACCELERATION_DECODE ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 8.52.322.1000 ", + "driverVersionMax": " 8.52.322.1112 ", + "driverVersionComparator": " BETWEEN_INCLUSIVE " + }, + + { + "os": "All", + "vendor": "0xdcdc", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBRTC_HW_ACCELERATION_ENCODE ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 8.52.322.1112 ", + "driverVersionMax": " 8.52.322.1200 ", + "driverVersionComparator": " BETWEEN_INCLUSIVE_START " + }, + { + "os": "All", + "vendor": "0xdcdc", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL_MSAA ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 8.52.322.1000 ", + "driverVersionMax": " 8.52.322.1200 ", + "driverVersionComparator": " BETWEEN_INCLUSIVE_START " + }, + + { + "os": "All", + "vendor": "0xdcdc", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL_ANGLE ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 8.52.322.1000 ", + "driverVersionMax": " 8.52.322.1112 ", + "driverVersionComparator": " BETWEEN_INCLUSIVE_START " + }, + + { + "os": "All", + "vendor": "0xdcdc", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL2 ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 8.52.322.1000 ", + "driverVersionMax": " 8.52.322.1112 ", + "driverVersionComparator": " BETWEEN_INCLUSIVE_START " + }, + + { + "os": "All", + "vendor": "0xdcdc", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " CANVAS2D_ACCELERATION ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 8.52.322.1000 ", + "driverVersionMax": " 9.52.322.1000 ", + "driverVersionComparator": " BETWEEN_EXCLUSIVE " + }, + + { + "os": "Android", + "vendor": "dcdc", + "devices": ["uiop"], + "feature": " DIRECT3D_11_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 5 ", + "driverVersionMax": " 6 ", + "driverVersionComparator": " BETWEEN_EXCLUSIVE " + }, + + { + "os": "Android", + "vendor": "dcdc", + "devices": ["uiop"], + "feature": " OPENGL_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 6 ", + "driverVersionMax": " 7 ", + "driverVersionComparator": " BETWEEN_EXCLUSIVE " + }, + + { + "os": "Android", + "vendor": "dcdc", + "devices": ["uiop"], + "feature": " DIRECT3D_11_ANGLE ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 5 ", + "driverVersionMax": " 7 ", + "driverVersionComparator": " BETWEEN_EXCLUSIVE " + }, + + { + "os": "Android", + "vendor": "dcdc", + "devices": ["uiop"], + "feature": " HARDWARE_VIDEO_DECODING ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 5 ", + "driverVersionMax": " 7 ", + "driverVersionComparator": " BETWEEN_INCLUSIVE " + }, + + { + "os": "Android", + "vendor": "dcdc", + "devices": ["uiop"], + "feature": " WEBRTC_HW_ACCELERATION_H264 ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 6 ", + "driverVersionMax": " 7 ", + "driverVersionComparator": " BETWEEN_INCLUSIVE " + }, + + { + "os": "Android", + "vendor": "dcdc", + "devices": ["uiop"], + "feature": " WEBRTC_HW_ACCELERATION_DECODE ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 5 ", + "driverVersionMax": " 6 ", + "driverVersionComparator": " BETWEEN_INCLUSIVE " + }, + + { + "os": "Android", + "vendor": "dcdc", + "devices": ["uiop"], + "feature": " WEBRTC_HW_ACCELERATION_ENCODE ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 6 ", + "driverVersionMax": " 7 ", + "driverVersionComparator": " BETWEEN_INCLUSIVE_START " + }, + { + "os": "Android", + "vendor": "dcdc", + "devices": ["uiop"], + "feature": " WEBGL_MSAA ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 5 ", + "driverVersionMax": " 7 ", + "driverVersionComparator": " BETWEEN_INCLUSIVE_START " + }, + + { + "os": "Android", + "vendor": "dcdc", + "devices": ["uiop"], + "feature": " WEBGL_ANGLE ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 5 ", + "driverVersionMax": " 6 ", + "driverVersionComparator": " BETWEEN_INCLUSIVE_START " + }, + + { + "os": "Android", + "vendor": "dcdc", + "devices": ["uiop"], + "feature": " WEBGL2 ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 5 ", + "driverVersionMax": " 6 ", + "driverVersionComparator": " BETWEEN_INCLUSIVE_START " + }, + + { + "os": "Android", + "vendor": "dcdc", + "devices": ["uiop"], + "feature": " CANVAS2D_ACCELERATION ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 5 ", + "driverVersionMax": " 7 ", + "driverVersionComparator": " BETWEEN_EXCLUSIVE " + } +] diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_AllOS.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_AllOS.json new file mode 100644 index 0000000000..3f44eb330f --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_AllOS.json @@ -0,0 +1,581 @@ +[ + { + "blockID": "g1", + "os": "All", + "vendor": "0xabcd", + "versionRange": { "minVersion": "15.0", "maxVersion": "15.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT2D ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "blockID": "g2", + "os": "All", + "vendor": "0xabcd", + "versionRange": { "minVersion": "15.0", "maxVersion": "22.0a1" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_9_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "All", + "vendor": "0xabcd", + "versionRange": { "minVersion": "16.0a1" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_10_LAYERS", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "All", + "vendor": "0xabcd", + "versionRange": { "minVersion": "16.0a1", "maxVersion": "22.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_10_1_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "All", + "vendor": "0xabcd", + "versionRange": { "minVersion": "12.0", "maxVersion": "16.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " OPENGL_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "blockID": "g11", + "os": "All", + "vendor": "0xabcd", + "versionRange": { "minVersion": "14.0b2", "maxVersion": "15.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL_OPENGL ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "All", + "vendor": "0xabcd", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL_ANGLE ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "All", + "vendor": "0xabcd", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL2 ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "All", + "vendor": "0xabcd", + "versionRange": { "minVersion": "12.0", "maxVersion": "16.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL_MSAA ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "All", + "vendor": "0xabcd", + "versionRange": { "maxVersion": "13.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " STAGEFRIGHT ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "All", + "vendor": "0xabcd", + "versionRange": { "minVersion": "42.0", "maxVersion": "13.0b2" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBRTC_HW_ACCELERATION_H264 ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "All", + "vendor": "0xabcd", + "versionRange": { "minVersion": "42.0", "maxVersion": "13.0b2" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBRTC_HW_ACCELERATION_ENCODE ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "All", + "vendor": "0xabcd", + "versionRange": { "minVersion": "42.0", "maxVersion": "13.0b2" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBRTC_HW_ACCELERATION_DECODE ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "All", + "vendor": "0xabcd", + "versionRange": { "minVersion": "17.2a2", "maxVersion": "15.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_11_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "All", + "vendor": "0xabcd", + "versionRange": { "minVersion": "15.0", "maxVersion": "13.2" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " HARDWARE_VIDEO_DECODING ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "All", + "vendor": "0xabcd", + "versionRange": { "minVersion": "10.5", "maxVersion": "13.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_11_ANGLE ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "blockID": "g1", + "os": "Darwin 13", + "vendor": "0xabcd", + "versionRange": { "minVersion": "15.0", "maxVersion": "15.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT2D ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "blockID": "g2", + "os": "Darwin 13", + "vendor": "0xabcd", + "versionRange": { "minVersion": "15.0", "maxVersion": "22.0a1" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_9_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Darwin 13", + "vendor": "0xabcd", + "versionRange": { "minVersion": "16.0a1" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_10_LAYERS", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Darwin 13", + "vendor": "0xabcd", + "versionRange": { "minVersion": "16.0a1", "maxVersion": "22.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_10_1_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Darwin 13", + "vendor": "0xabcd", + "versionRange": { "minVersion": "12.0", "maxVersion": "16.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " OPENGL_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "blockID": "g11", + "os": "Darwin 13", + "vendor": "0xabcd", + "versionRange": { "minVersion": "14.0b2", "maxVersion": "15.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL_OPENGL ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Darwin 13", + "vendor": "0xabcd", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL_ANGLE ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Darwin 13", + "vendor": "0xabcd", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL2 ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Darwin 13", + "vendor": "0xabcd", + "versionRange": { "minVersion": "12.0", "maxVersion": "16.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL_MSAA ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Darwin 13", + "vendor": "0xabcd", + "versionRange": { "maxVersion": "13.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " STAGEFRIGHT ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Darwin 13", + "vendor": "0xabcd", + "versionRange": { "minVersion": "42.0", "maxVersion": "13.0b2" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBRTC_HW_ACCELERATION_H264 ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Darwin 13", + "vendor": "0xabcd", + "versionRange": { "minVersion": "42.0", "maxVersion": "13.0b2" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBRTC_HW_ACCELERATION_ENCODE ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Darwin 13", + "vendor": "0xabcd", + "versionRange": { "minVersion": "42.0", "maxVersion": "13.0b2" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBRTC_HW_ACCELERATION_DECODE ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Darwin 13", + "vendor": "0xabcd", + "versionRange": { "minVersion": "17.2a2", "maxVersion": "15.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_11_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Darwin 13", + "vendor": "0xabcd", + "versionRange": { "minVersion": "15.0", "maxVersion": "13.2" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " HARDWARE_VIDEO_DECODING ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Darwin 13", + "vendor": "0xabcd", + "versionRange": { "minVersion": "10.5", "maxVersion": "13.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_11_ANGLE ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "blockID": "g1", + "os": "Linux", + "vendor": "0xabcd", + "versionRange": { "minVersion": "15.0", "maxVersion": "15.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT2D ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "blockID": "g2", + "os": "Linux", + "vendor": "0xabcd", + "versionRange": { "minVersion": "15.0", "maxVersion": "22.0a1" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_9_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Linux", + "vendor": "0xabcd", + "versionRange": { "minVersion": "16.0a1" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_10_LAYERS", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Linux", + "vendor": "0xabcd", + "versionRange": { "minVersion": "16.0a1", "maxVersion": "22.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_10_1_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Linux", + "vendor": "0xabcd", + "versionRange": { "minVersion": "12.0", "maxVersion": "16.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " OPENGL_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "blockID": "g11", + "os": "Linux", + "vendor": "0xabcd", + "versionRange": { "minVersion": "14.0b2", "maxVersion": "15.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL_OPENGL ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Linux", + "vendor": "0xabcd", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL_ANGLE ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Linux", + "vendor": "0xabcd", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL2 ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Linux", + "vendor": "0xabcd", + "versionRange": { "minVersion": "12.0", "maxVersion": "16.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL_MSAA ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Linux", + "vendor": "0xabcd", + "versionRange": { "maxVersion": "13.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " STAGEFRIGHT ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Linux", + "vendor": "0xabcd", + "versionRange": { "minVersion": "42.0", "maxVersion": "13.0b2" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBRTC_HW_ACCELERATION_H264 ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Linux", + "vendor": "0xabcd", + "versionRange": { "minVersion": "42.0", "maxVersion": "13.0b2" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBRTC_HW_ACCELERATION_ENCODE ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Linux", + "vendor": "0xabcd", + "versionRange": { "minVersion": "42.0", "maxVersion": "13.0b2" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBRTC_HW_ACCELERATION_DECODE ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Linux", + "vendor": "0xabcd", + "versionRange": { "minVersion": "17.2a2", "maxVersion": "15.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_11_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Linux", + "vendor": "0xabcd", + "versionRange": { "minVersion": "15.0", "maxVersion": "13.2" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " HARDWARE_VIDEO_DECODING ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Linux", + "vendor": "0xabcd", + "versionRange": { "minVersion": "10.5", "maxVersion": "13.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_11_ANGLE ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "blockID": "g1", + "os": "Android", + "vendor": "0xabcd", + "versionRange": { "minVersion": "15.0", "maxVersion": "15.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT2D ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "blockID": "g2", + "os": "Android", + "vendor": "0xabcd", + "versionRange": { "minVersion": "15.0", "maxVersion": "22.0a1" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_9_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Android", + "vendor": "0xabcd", + "versionRange": { "minVersion": "16.0a1" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_10_LAYERS", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Android", + "vendor": "0xabcd", + "versionRange": { "minVersion": "16.0a1", "maxVersion": "22.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_10_1_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Android", + "vendor": "0xabcd", + "versionRange": { "minVersion": "12.0", "maxVersion": "16.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " OPENGL_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "blockID": "g11", + "os": "Android", + "vendor": "0xabcd", + "versionRange": { "minVersion": "14.0b2", "maxVersion": "15.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL_OPENGL ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Android", + "vendor": "0xabcd", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL_ANGLE ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Android", + "vendor": "0xabcd", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL2 ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Android", + "vendor": "0xabcd", + "versionRange": { "minVersion": "12.0", "maxVersion": "16.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBGL_MSAA ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Android", + "vendor": "0xabcd", + "versionRange": { "maxVersion": "13.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " STAGEFRIGHT ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Android", + "vendor": "0xabcd", + "versionRange": { "minVersion": "42.0", "maxVersion": "13.0b2" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBRTC_HW_ACCELERATION_H264 ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Android", + "vendor": "0xabcd", + "versionRange": { "minVersion": "42.0", "maxVersion": "13.0b2" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBRTC_HW_ACCELERATION_ENCODE ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Android", + "vendor": "0xabcd", + "versionRange": { "minVersion": "42.0", "maxVersion": "13.0b2" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " WEBRTC_HW_ACCELERATION_DECODE ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Android", + "vendor": "0xabcd", + "versionRange": { "minVersion": "17.2a2", "maxVersion": "15.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_11_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Android", + "vendor": "0xabcd", + "versionRange": { "minVersion": "15.0", "maxVersion": "13.2" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " HARDWARE_VIDEO_DECODING ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + }, + + { + "os": "Android", + "vendor": "0xabcd", + "versionRange": { "minVersion": "10.5", "maxVersion": "13.0" }, + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT3D_11_ANGLE ", + "featureStatus": " BLOCKED_DRIVER_VERSION " + } +] diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_OSVersion.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_OSVersion.json new file mode 100644 index 0000000000..c80bf3eedd --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_OSVersion.json @@ -0,0 +1,20 @@ +[ + { + "os": "WINNT 6.2", + "vendor": "0xabcd", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " DIRECT2D ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 8.52.322.2202 ", + "driverVersionComparator": " LESS_THAN " + }, + { + "os": "Darwin 13", + "vendor": "0xabcd", + "devices": ["0x2783", "0x1234", "0x2782"], + "feature": " OPENGL_LAYERS ", + "featureStatus": " BLOCKED_DRIVER_VERSION ", + "driverVersion": " 8.52.322.2202 ", + "driverVersionComparator": " LESS_THAN " + } +] diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_install_addons.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_install_addons.json new file mode 100644 index 0000000000..d7307831af --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_install_addons.json @@ -0,0 +1,31 @@ +{ + "page_size": 25, + "page_count": 1, + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "name": "Real Test 2", + "type": "extension", + "guid": "addon2@tests.mozilla.org", + "current_version": { + "version": "1.0", + "files": [ + { + "size": 2, + "url": "http://example.com/browser/toolkit/mozapps/extensions/test/browser/addons/browser_install1_2.xpi" + } + ] + }, + "authors": [ + { + "name": "Test Creator", + "url": "http://example.com/creator.html" + } + ], + "summary": "Repository summary", + "description": "Repository description" + } + ] +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_install_compat.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_install_compat.json new file mode 100644 index 0000000000..93d0cf3d3d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_install_compat.json @@ -0,0 +1,27 @@ +{ + "page_size": 25, + "page_count": 1, + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "addon_guid": "addon6@tests.mozilla.org", + "name": "Addon Test 6", + "version_ranges": [ + { + "addon_min_version": "1.0", + "addon_max_version": "1.0", + "applications": [ + { + "name": "XPCShell", + "guid": "xpcshell@tests.mozilla.org", + "min_version": "1.0", + "max_version": "1.0" + } + ] + } + ] + } + ] +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_no_update.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_no_update.json new file mode 100644 index 0000000000..2773c7f98f --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_no_update.json @@ -0,0 +1,7 @@ +{ + "addons": { + "test_no_update_webext@tests.mozilla.org": { + "updates": [] + } + } +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/ancient.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/ancient.xml new file mode 100644 index 0000000000..699257f87e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/ancient.xml @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <emItems> + <emItem blockID="i454" id="ancient@tests.mozilla.org"> + <versionRange minVersion="0" maxVersion="*" severity="3"/> + </emItem> + </emItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/new.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/new.xml new file mode 100644 index 0000000000..8cbfb5d6a0 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/new.xml @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1396046918000"> + <emItems> + <emItem blockID="i454" id="new@tests.mozilla.org"> + <versionRange minVersion="0" maxVersion="*" severity="3"/> + </emItem> + </emItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/old.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/old.xml new file mode 100644 index 0000000000..75bd6e934c --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/old.xml @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1296046918000"> + <emItems> + <emItem blockID="i454" id="old@tests.mozilla.org"> + <versionRange minVersion="0" maxVersion="*" severity="3"/> + </emItem> + </emItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtp.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtp.xml new file mode 100644 index 0000000000..937d8a5901 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtp.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <pluginItems> + <pluginItem> + <match name="name" exp="^test_plugin_0"/> + <versionRange minVersion="0" maxVersion="*" severity="0"/> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_plugin_1"/> + <versionRange minVersion="0" maxVersion="*" severity="0"/> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_plugin_2"/> + <versionRange minVersion="0" maxVersion="*" severity="0"/> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_plugin_3"/> + <versionRange minVersion="0" maxVersion="*"/> + </pluginItem> + <pluginItem> + <match name="name" exp="^test_plugin_4"/> + <versionRange minVersion="0" maxVersion="*" severity="1"/> + </pluginItem> + </pluginItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtpUndo.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtpUndo.xml new file mode 100644 index 0000000000..162876230e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtpUndo.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <pluginItems> + <pluginItem> + <match name="name" exp="^Test Plug-in"/> + <versionRange minVersion="0" maxVersion="*" severity="0"/> + </pluginItem> + </pluginItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_softblocked1.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_softblocked1.xml new file mode 100644 index 0000000000..a1d18470c8 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_softblocked1.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <emItems> + <emItem id="softblock1@tests.mozilla.org"> + <versionRange severity="1"/> + </emItem> + </emItems> +</blocklist> diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_trash_directory.worker.js b/toolkit/mozapps/extensions/test/xpcshell/data/test_trash_directory.worker.js new file mode 100644 index 0000000000..9814d5bc96 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_trash_directory.worker.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/. */ + +/* import-globals-from /toolkit/components/workerloader/require.js */ +importScripts("resource://gre/modules/workers/require.js"); + +const PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js"); + +class OpenFileWorker extends PromiseWorker.AbstractWorker { + constructor() { + super(); + + this._file = null; + } + + postMessage(message, ...transfers) { + self.postMessage(message, transfers); + } + + dispatch(method, args) { + return this[method](...args); + } + + open(path) { + this._file = IOUtils.openFileForSyncReading(path); + } + + close() { + if (this._file) { + this._file.close(); + } + } +} + +const worker = new OpenFileWorker(); + +self.addEventListener("message", msg => worker.handleMessage(msg)); +self.addEventListener("unhandledrejection", err => { + throw err.reason; +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_update.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_update.json new file mode 100644 index 0000000000..930ed44e5d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_update.json @@ -0,0 +1,120 @@ +{ + "addons": { + "addon1@tests.mozilla.org": { + "updates": [ + { + "version": "1.0", + "applications": { + "gecko": { + "strict_min_version": "1" + } + } + }, + { + "version": "1.0", + "applications": { + "gecko": { + "strict_min_version": "2" + } + } + }, + { + "version": "2.0", + "update_link": "http://example.com/addons/test_update.xpi", + "update_info_url": "http://example.com/updateInfo.xhtml", + "applications": { + "gecko": { + "strict_min_version": "1" + } + } + } + ] + }, + + "addon2@tests.mozilla.org": { + "updates": [ + { + "version": "1.0", + "applications": { + "gecko": { + "strict_min_version": "0", + "advisory_max_version": "1" + } + } + } + ] + }, + + "addon3@tests.mozilla.org": { + "updates": [ + { + "version": "1.0", + "applications": { + "gecko": { + "strict_min_version": "3", + "advisory_max_version": "3" + } + } + } + ] + }, + + "addon4@tests.mozilla.org": { + "updates": [ + { + "version": "5.0", + "applications": { + "gecko": { + "strict_min_version": "0", + "advisory_max_version": "0" + } + } + } + ] + }, + + "addon7@tests.mozilla.org": { + "updates": [ + { + "version": "1.0", + "applications": { + "gecko": { + "strict_min_version": "0", + "advisory_max_version": "1" + } + } + } + ] + }, + + "addon8@tests.mozilla.org": { + "updates": [ + { + "version": "2.0", + "update_link": "http://example.com/addons/test_update8.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "advisory_max_version": "1" + } + } + } + ] + }, + + "addon12@tests.mozilla.org": { + "updates": [ + { + "version": "2.0", + "update_link": "http://example.com/addons/test_update12.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "advisory_max_version": "1" + } + } + } + ] + } + } +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_update_addons.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_update_addons.json new file mode 100644 index 0000000000..d9777335a6 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_update_addons.json @@ -0,0 +1,14 @@ +{ + "page_size": 25, + "page_count": 1, + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "name": "Ttest Addon 9", + "type": "extension", + "guid": "addon9@tests.mozilla.org" + } + ] +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_update_compat.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_update_compat.json new file mode 100644 index 0000000000..cc2cc15ad5 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_update_compat.json @@ -0,0 +1,28 @@ +{ + "page_size": 25, + "page_count": 1, + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "addon_guid": "addon9@tests.mozilla.org", + "name": "Test Addon 9", + "version_ranges": [ + { + "addon_min_version": "4", + "addon_max_version": "4", + "applications": [ + { + "name": "XPCShell", + "id": "XPCShell", + "guid": "xpcshell@tests.mozilla.org", + "min_version": "1", + "max_version": "1" + } + ] + } + ] + } + ] +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.json new file mode 100644 index 0000000000..f61bfeacd3 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.json @@ -0,0 +1,269 @@ +{ + "addons": { + "updatecheck1@tests.mozilla.org": { + "updates": [ + { + "version": "1.0", + "update_link": "https://example.com/addons/test1.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + }, + { + "_comment_": "This update is incompatible and so should not be considered a valid update", + "version": "2.0", + "update_link": "https://example.com/addons/test2.xpi", + "applications": { + "gecko": { + "strict_min_version": "2", + "strict_max_version": "2" + } + } + }, + { + "version": "3.0", + "update_link": "https://example.com/addons/test3.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + }, + { + "version": "2.0", + "update_link": "https://example.com/addons/test2.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "2" + } + } + }, + { + "_comment_": "This update is incompatible and so should not be considered a valid update", + "version": "4.0", + "update_link": "https://example.com/addons/test4.xpi", + "applications": { + "gecko": { + "strict_min_version": "2", + "strict_max_version": "2" + } + } + } + ] + }, + + "test_bug378216_8@tests.mozilla.org": { + "_comment_": "The updateLink will be ignored since it is not secure and there is no updateHash.", + + "updates": [ + { + "version": "2.0", + "update_link": "http://example.com/broken.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + } + ] + }, + + "test_bug378216_9@tests.mozilla.org": { + "_comment_": "The updateLink will used since there is an updateHash to verify it.", + + "updates": [ + { + "version": "2.0", + "update_link": "http://example.com/broken.xpi", + "update_hash": "sha256:78fc1d2887eda35b4ad2e3a0b60120ca271ce6e6", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + } + ] + }, + + "test_bug378216_10@tests.mozilla.org": { + "_comment_": "The updateLink will used since it is a secure URL.", + + "updates": [ + { + "version": "2.0", + "update_link": "https://example.com/broken.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + } + ] + }, + + "test_bug378216_11@tests.mozilla.org": { + "_comment_": "The updateLink will used since it is a secure URL.", + + "updates": [ + { + "version": "2.0", + "update_link": "https://example.com/broken.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + } + ] + }, + + "test_bug378216_12@tests.mozilla.org": { + "_comment_": "The updateLink will not be used since the updateHash verifying it is not strong enough.", + + "updates": [ + { + "version": "2.0", + "update_link": "http://example.com/broken.xpi", + "update_hash": "sha1:78fc1d2887eda35b4ad2e3a0b60120ca271ce6e6", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + } + ] + }, + + "test_bug378216_13@tests.mozilla.org": { + "_comment_": "An update with a weak hash. The updateLink will used since it is a secure URL.", + + "updates": [ + { + "version": "2.0", + "update_link": "https://example.com/broken.xpi", + "update_hash": "sha1:78fc1d2887eda35b4ad2e3a0b60120ca271ce6e6", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + } + ] + }, + + "_comment_": "There should be no information present for test_bug378216_14", + + "test_bug378216_15@tests.mozilla.org": { + "_comment_": "Invalid update JSON", + + "updates": "foo" + }, + + "ignore-compat@tests.mozilla.org": { + "_comment_": "Various updates available - one is not compatible, but compatibility checking is disabled", + + "updates": [ + { + "version": "1.0", + "update_link": "https://example.com/addons/test1.xpi", + "applications": { + "gecko": { + "strict_min_version": "0.1", + "advisory_max_version": "0.2" + } + } + }, + { + "version": "2.0", + "update_link": "https://example.com/addons/test2.xpi", + "applications": { + "gecko": { + "strict_min_version": "0.5", + "advisory_max_version": "0.6" + } + } + }, + { + "_comment_": "Update for future app versions - should never be compatible", + "version": "3.0", + "update_link": "https://example.com/addons/test3.xpi", + "applications": { + "gecko": { + "strict_min_version": "2", + "advisory_max_version": "3" + } + } + } + ] + }, + + "compat-override@tests.mozilla.org": { + "_comment_": "Various updates available - one is not compatible, but compatibility checking is disabled", + + "updates": [ + { + "_comment_": "Has compatibility override, but it doesn't match this app version", + "version": "1.0", + "update_link": "https://example.com/addons/test1.xpi", + "applications": { + "gecko": { + "strict_min_version": "0.1", + "advisory_max_version": "0.2" + } + } + }, + { + "_comment_": "Has compatibility override, so is incompaible", + "version": "2.0", + "update_link": "https://example.com/addons/test2.xpi", + "applications": { + "gecko": { + "strict_min_version": "0.5", + "advisory_max_version": "0.6" + } + } + }, + { + "_comment_": "Update for future app versions - should never be compatible", + "version": "3.0", + "update_link": "https://example.com/addons/test3.xpi", + "applications": { + "gecko": { + "strict_min_version": "2", + "advisory_max_version": "3" + } + } + } + ] + }, + + "compat-strict-optin@tests.mozilla.org": { + "_comment_": "Opt-in to strict compatibility checking", + + "updates": [ + { + "version": "1.0", + "update_link": "https://example.com/addons/test1.xpi", + "_comment_": "strictCompatibility: true", + "applications": { + "gecko": { + "strict_min_version": "0.1", + "strict_max_version": "0.2" + } + } + } + ] + } + } +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/unsigned.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/unsigned.xpi Binary files differnew file mode 100644 index 0000000000..12a13f139b --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/unsigned.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/webext-implicit-id.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/webext-implicit-id.xpi Binary files differnew file mode 100644 index 0000000000..6b4abaa691 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/webext-implicit-id.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js new file mode 100644 index 0000000000..23614cdb2a --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js @@ -0,0 +1,1223 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* eslint no-unused-vars: ["error", {vars: "local", args: "none"}] */ + +if (!_TEST_NAME.includes("toolkit/mozapps/extensions/test/xpcshell/")) { + Assert.ok( + false, + "head_addons.js may not be loaded by tests outside of " + + "the add-on manager component." + ); +} + +const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity"; +const PREF_EM_STRICT_COMPATIBILITY = "extensions.strictCompatibility"; +const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url"; +const PREF_XPI_SIGNATURES_REQUIRED = "xpinstall.signatures.required"; + +// Maximum error in file modification times. Some file systems don't store +// modification times exactly. As long as we are closer than this then it +// still passes. +const MAX_TIME_DIFFERENCE = 3000; + +// Time to reset file modified time relative to Date.now() so we can test that +// times are modified (10 hours old). +const MAKE_FILE_OLD_DIFFERENCE = 10 * 3600 * 1000; + +const { AddonManager, AddonManagerPrivate } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +var { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +var { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { AddonRepository } = ChromeUtils.importESModule( + "resource://gre/modules/addons/AddonRepository.sys.mjs" +); + +var { AddonTestUtils, MockAsyncShutdown } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + Blocklist: "resource://gre/modules/Blocklist.sys.mjs", + Extension: "resource://gre/modules/Extension.sys.mjs", + ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.sys.mjs", + ExtensionTestUtils: + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs", + HttpServer: "resource://testing-common/httpd.sys.mjs", + MockRegistrar: "resource://testing-common/MockRegistrar.sys.mjs", + MockRegistry: "resource://testing-common/MockRegistry.sys.mjs", + PromiseTestUtils: "resource://testing-common/PromiseTestUtils.sys.mjs", + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + TestUtils: "resource://testing-common/TestUtils.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +XPCOMUtils.defineLazyServiceGetter( + this, + "aomStartup", + "@mozilla.org/addons/addon-manager-startup;1", + "amIAddonManagerStartup" +); + +const { + createAppInfo, + createHttpServer, + createTempWebExtensionFile, + getFileForAddon, + manuallyInstall, + manuallyUninstall, + overrideBuiltIns, + promiseAddonEvent, + promiseCompleteAllInstalls, + promiseCompleteInstall, + promiseConsoleOutput, + promiseFindAddonUpdates, + promiseInstallAllFiles, + promiseInstallFile, + promiseRestartManager, + promiseSetExtensionModifiedTime, + promiseShutdownManager, + promiseStartupManager, + promiseWebExtensionStartup, + promiseWriteProxyFileToDir, + registerDirectory, + setExtensionModifiedTime, + writeFilesToZip, +} = AddonTestUtils; + +// WebExtension wrapper for ease of testing +ExtensionTestUtils.init(this); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); + +ChromeUtils.defineLazyGetter( + this, + "BOOTSTRAP_REASONS", + () => AddonManagerPrivate.BOOTSTRAP_REASONS +); + +function getReasonName(reason) { + for (let key of Object.keys(BOOTSTRAP_REASONS)) { + if (BOOTSTRAP_REASONS[key] == reason) { + return key; + } + } + throw new Error("This shouldn't happen."); +} + +Object.defineProperty(this, "gAppInfo", { + get() { + return AddonTestUtils.appInfo; + }, +}); + +Object.defineProperty(this, "gAddonStartup", { + get() { + return AddonTestUtils.addonStartup.clone(); + }, +}); + +Object.defineProperty(this, "gInternalManager", { + get() { + return AddonTestUtils.addonIntegrationService.QueryInterface( + Ci.nsITimerCallback + ); + }, +}); + +Object.defineProperty(this, "gProfD", { + get() { + return AddonTestUtils.profileDir.clone(); + }, +}); + +Object.defineProperty(this, "gTmpD", { + get() { + return AddonTestUtils.tempDir.clone(); + }, +}); + +Object.defineProperty(this, "gUseRealCertChecks", { + get() { + return AddonTestUtils.useRealCertChecks; + }, + set(val) { + AddonTestUtils.useRealCertChecks = val; + }, +}); + +Object.defineProperty(this, "TEST_UNPACKED", { + get() { + return AddonTestUtils.testUnpacked; + }, + set(val) { + AddonTestUtils.testUnpacked = val; + }, +}); + +const promiseAddonByID = AddonManager.getAddonByID; +const promiseAddonsByIDs = AddonManager.getAddonsByIDs; +const promiseAddonsByTypes = AddonManager.getAddonsByTypes; + +var gPort = null; + +var BootstrapMonitor = { + started: new Map(), + stopped: new Map(), + installed: new Map(), + uninstalled: new Map(), + + init() { + this.onEvent = this.onEvent.bind(this); + + AddonTestUtils.on("addon-manager-shutdown", this.onEvent); + AddonTestUtils.on("bootstrap-method", this.onEvent); + }, + + shutdownCheck() { + equal( + this.started.size, + 0, + "Should have no add-ons that were started but not shutdown" + ); + }, + + onEvent(msg, data) { + switch (msg) { + case "addon-manager-shutdown": + this.shutdownCheck(); + break; + case "bootstrap-method": + this.onBootstrapMethod(data.method, data.params, data.reason); + break; + } + }, + + onBootstrapMethod(method, params, reason) { + let { id } = params; + + info( + `Bootstrap method ${method} ${reason} for ${params.id} version ${params.version}` + ); + + if (method !== "install") { + this.checkInstalled(id); + } + + switch (method) { + case "install": + this.checkNotInstalled(id); + this.installed.set(id, { reason, params }); + this.uninstalled.delete(id); + break; + case "startup": + this.checkNotStarted(id); + this.started.set(id, { reason, params }); + this.stopped.delete(id); + break; + case "shutdown": + this.checkMatches("shutdown", "startup", params, this.started.get(id)); + this.checkStarted(id); + this.stopped.set(id, { reason, params }); + this.started.delete(id); + break; + case "uninstall": + this.checkMatches( + "uninstall", + "install", + params, + this.installed.get(id) + ); + this.uninstalled.set(id, { reason, params }); + this.installed.delete(id); + break; + case "update": + this.checkMatches("update", "install", params, this.installed.get(id)); + this.installed.set(id, { reason, params, method }); + break; + } + }, + + clear(id) { + this.installed.delete(id); + this.started.delete(id); + this.stopped.delete(id); + this.uninstalled.delete(id); + }, + + checkMatches(method, lastMethod, params, { params: lastParams } = {}) { + ok( + lastParams, + `Expecting matching ${lastMethod} call for add-on ${params.id} ${method} call` + ); + + if (method == "update") { + equal( + params.oldVersion, + lastParams.version, + "params.oldVersion should match last call" + ); + } else { + equal( + params.version, + lastParams.version, + "params.version should match last call" + ); + } + + if (method !== "update" && method !== "uninstall") { + equal( + params.resourceURI.spec, + lastParams.resourceURI.spec, + `params.resourceURI should match last call` + ); + + ok( + params.resourceURI.equals(lastParams.resourceURI), + `params.resourceURI should match: "${params.resourceURI.spec}" == "${lastParams.resourceURI.spec}"` + ); + } + }, + + checkStarted(id, version = undefined) { + let started = this.started.get(id); + ok(started, `Should have seen startup method call for ${id}`); + + if (version !== undefined) { + equal(started.params.version, version, "Expected version number"); + } + return started; + }, + + checkNotStarted(id) { + ok( + !this.started.has(id), + `Should not have seen startup method call for ${id}` + ); + }, + + checkInstalled(id, version = undefined) { + const installed = this.installed.get(id); + ok(installed, `Should have seen install call for ${id}`); + + if (version !== undefined) { + equal(installed.params.version, version, "Expected version number"); + } + + return installed; + }, + + checkUpdated(id, version = undefined) { + const installed = this.installed.get(id); + equal(installed.method, "update", `Should have seen update call for ${id}`); + + if (version !== undefined) { + equal(installed.params.version, version, "Expected version number"); + } + + return installed; + }, + + checkNotInstalled(id) { + ok( + !this.installed.has(id), + `Should not have seen install method call for ${id}` + ); + }, +}; + +function isNightlyChannel() { + var channel = Services.prefs.getCharPref("app.update.channel", "default"); + + return ( + channel != "aurora" && + channel != "beta" && + channel != "release" && + channel != "esr" + ); +} + +async function restartWithLocales(locales) { + Services.locale.requestedLocales = locales; + await promiseRestartManager(); +} + +function delay(msec) { + return new Promise(resolve => { + setTimeout(resolve, msec); + }); +} + +/** + * Returns a map of Addon objects for installed add-ons with the given + * IDs. The returned map contains a key for the ID of each add-on that + * is found. IDs for add-ons which do not exist are not present in the + * map. + * + * @param {sequence<string>} ids + * The list of add-on IDs to get. + * @returns {Promise<string, Addon>} + * Map of add-ons that were found. + */ +async function getAddons(ids) { + let addons = new Map(); + for (let addon of await AddonManager.getAddonsByIDs(ids)) { + if (addon) { + addons.set(addon.id, addon); + } + } + return addons; +} + +/** + * Checks that the given add-on has the given expected properties. + * + * @param {string} id + * The id of the add-on. + * @param {Addon?} addon + * The add-on object, or null if the add-on does not exist. + * @param {object?} expected + * An object containing the expected values for properties of the + * add-on, or null if the add-on is expected not to exist. + */ +function checkAddon(id, addon, expected) { + info(`Checking state of addon ${id}`); + + if (expected === null) { + ok(!addon, `Addon ${id} should not exist`); + } else { + ok(addon, `Addon ${id} should exist`); + for (let [key, value] of Object.entries(expected)) { + if (value instanceof Ci.nsIURI) { + equal( + addon[key] && addon[key].spec, + value.spec, + `Expected value of addon.${key}` + ); + } else { + deepEqual(addon[key], value, `Expected value of addon.${key}`); + } + } + } +} + +/** + * Tests that an add-on does appear in the crash report annotations, if + * crash reporting is enabled. The test will fail if the add-on is not in the + * annotation. + * @param aId + * The ID of the add-on + * @param aVersion + * The version of the add-on + */ +function do_check_in_crash_annotation(aId, aVersion) { + if (!AppConstants.MOZ_CRASHREPORTER) { + return; + } + + if (!("Add-ons" in gAppInfo.annotations)) { + Assert.ok(false, "Cannot find Add-ons entry in crash annotations"); + return; + } + + let addons = gAppInfo.annotations["Add-ons"].split(","); + Assert.ok( + addons.includes( + `${encodeURIComponent(aId)}:${encodeURIComponent(aVersion)}` + ) + ); +} + +/** + * Tests that an add-on does not appear in the crash report annotations, if + * crash reporting is enabled. The test will fail if the add-on is in the + * annotation. + * @param aId + * The ID of the add-on + * @param aVersion + * The version of the add-on + */ +function do_check_not_in_crash_annotation(aId, aVersion) { + if (!AppConstants.MOZ_CRASHREPORTER) { + return; + } + + if (!("Add-ons" in gAppInfo.annotations)) { + Assert.ok(true); + return; + } + + let addons = gAppInfo.annotations["Add-ons"].split(","); + Assert.ok( + !addons.includes( + `${encodeURIComponent(aId)}:${encodeURIComponent(aVersion)}` + ) + ); +} + +function do_get_file_hash(aFile, aAlgorithm) { + if (!aAlgorithm) { + aAlgorithm = "sha256"; + } + + let crypto = Cc["@mozilla.org/security/hash;1"].createInstance( + Ci.nsICryptoHash + ); + crypto.initWithString(aAlgorithm); + let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fis.init(aFile, -1, -1, false); + crypto.updateFromStream(fis, aFile.fileSize); + + // return the two-digit hexadecimal code for a byte + let toHexString = charCode => ("0" + charCode.toString(16)).slice(-2); + + let binary = crypto.finish(false); + let hash = Array.from(binary, c => toHexString(c.charCodeAt(0))); + return aAlgorithm + ":" + hash.join(""); +} + +/** + * Returns an extension uri spec + * + * @param aProfileDir + * The extension install directory + * @return a uri spec pointing to the root of the extension + */ +function do_get_addon_root_uri(aProfileDir, aId) { + let path = aProfileDir.clone(); + path.append(aId); + if (!path.exists()) { + path.leafName += ".xpi"; + return "jar:" + Services.io.newFileURI(path).spec + "!/"; + } + return Services.io.newFileURI(path).spec; +} + +function do_get_expected_addon_name(aId) { + if (TEST_UNPACKED) { + return aId; + } + return aId + ".xpi"; +} + +/** + * Returns the file containing the add-on. For packed add-ons, this is + * an XPI file. For unpacked add-ons, it is the add-on's root directory. + * + * @param {Addon} addon + * @returns {nsIFile} + */ +function getAddonFile(addon) { + let uri = addon.getResourceURI(""); + if (uri instanceof Ci.nsIJARURI) { + uri = uri.JARFile; + } + return uri.QueryInterface(Ci.nsIFileURL).file; +} + +/** + * Check that an array of actual add-ons is the same as an array of + * expected add-ons. + * + * @param aActualAddons + * The array of actual add-ons to check. + * @param aExpectedAddons + * The array of expected add-ons to check against. + * @param aProperties + * An array of properties to check. + */ +function do_check_addons(aActualAddons, aExpectedAddons, aProperties) { + Assert.notEqual(aActualAddons, null); + Assert.equal(aActualAddons.length, aExpectedAddons.length); + for (let i = 0; i < aActualAddons.length; i++) { + do_check_addon(aActualAddons[i], aExpectedAddons[i], aProperties); + } +} + +/** + * Check that the actual add-on is the same as the expected add-on. + * + * @param aActualAddon + * The actual add-on to check. + * @param aExpectedAddon + * The expected add-on to check against. + * @param aProperties + * An array of properties to check. + */ +function do_check_addon(aActualAddon, aExpectedAddon, aProperties) { + Assert.notEqual(aActualAddon, null); + + aProperties.forEach(function (aProperty) { + let actualValue = aActualAddon[aProperty]; + let expectedValue = aExpectedAddon[aProperty]; + + // Check that all undefined expected properties are null on actual add-on + if (!(aProperty in aExpectedAddon)) { + if (actualValue !== undefined && actualValue !== null) { + do_throw( + "Unexpected defined/non-null property for add-on " + + aExpectedAddon.id + + " (addon[" + + aProperty + + "] = " + + actualValue.toSource() + + ")" + ); + } + + return; + } else if (expectedValue && !actualValue) { + do_throw( + "Missing property for add-on " + + aExpectedAddon.id + + ": expected addon[" + + aProperty + + "] = " + + expectedValue + ); + return; + } + + switch (aProperty) { + case "creator": + do_check_author(actualValue, expectedValue); + break; + + case "developers": + Assert.equal(actualValue.length, expectedValue.length); + for (let i = 0; i < actualValue.length; i++) { + do_check_author(actualValue[i], expectedValue[i]); + } + break; + + case "screenshots": + Assert.equal(actualValue.length, expectedValue.length); + for (let i = 0; i < actualValue.length; i++) { + do_check_screenshot(actualValue[i], expectedValue[i]); + } + break; + + case "sourceURI": + Assert.equal(actualValue.spec, expectedValue); + break; + + case "updateDate": + Assert.equal(actualValue.getTime(), expectedValue.getTime()); + break; + + case "compatibilityOverrides": + Assert.equal(actualValue.length, expectedValue.length); + for (let i = 0; i < actualValue.length; i++) { + do_check_compatibilityoverride(actualValue[i], expectedValue[i]); + } + break; + + case "icons": + do_check_icons(actualValue, expectedValue); + break; + + default: + if (actualValue !== expectedValue) { + do_throw( + "Failed for " + + aProperty + + " for add-on " + + aExpectedAddon.id + + " (" + + actualValue + + " === " + + expectedValue + + ")" + ); + } + } + }); +} + +/** + * Check that the actual author is the same as the expected author. + * + * @param aActual + * The actual author to check. + * @param aExpected + * The expected author to check against. + */ +function do_check_author(aActual, aExpected) { + Assert.equal(aActual.toString(), aExpected.name); + Assert.equal(aActual.name, aExpected.name); + Assert.equal(aActual.url, aExpected.url); +} + +/** + * Check that the actual screenshot is the same as the expected screenshot. + * + * @param aActual + * The actual screenshot to check. + * @param aExpected + * The expected screenshot to check against. + */ +function do_check_screenshot(aActual, aExpected) { + Assert.equal(aActual.toString(), aExpected.url); + Assert.equal(aActual.url, aExpected.url); + Assert.equal(aActual.width, aExpected.width); + Assert.equal(aActual.height, aExpected.height); + Assert.equal(aActual.thumbnailURL, aExpected.thumbnailURL); + Assert.equal(aActual.thumbnailWidth, aExpected.thumbnailWidth); + Assert.equal(aActual.thumbnailHeight, aExpected.thumbnailHeight); + Assert.equal(aActual.caption, aExpected.caption); +} + +/** + * Check that the actual compatibility override is the same as the expected + * compatibility override. + * + * @param aAction + * The actual compatibility override to check. + * @param aExpected + * The expected compatibility override to check against. + */ +function do_check_compatibilityoverride(aActual, aExpected) { + Assert.equal(aActual.type, aExpected.type); + Assert.equal(aActual.minVersion, aExpected.minVersion); + Assert.equal(aActual.maxVersion, aExpected.maxVersion); + Assert.equal(aActual.appID, aExpected.appID); + Assert.equal(aActual.appMinVersion, aExpected.appMinVersion); + Assert.equal(aActual.appMaxVersion, aExpected.appMaxVersion); +} + +function do_check_icons(aActual, aExpected) { + for (var size in aExpected) { + Assert.equal(aActual[size], aExpected[size]); + } +} + +function isThemeInAddonsList(aDir, aId) { + return AddonTestUtils.addonsList.hasTheme(aDir, aId); +} + +function isExtensionInBootstrappedList(aDir, aId) { + return AddonTestUtils.addonsList.hasExtension(aDir, aId); +} + +/** + * Writes a manifest.json manifest into an extension using the properties passed + * in a JS object. + * + * @param aManifest + * The data to write + * @param aDir + * The install directory to add the extension to + * @param aId + * An optional string to override the default installation aId + * @return A file pointing to where the extension was installed + */ +function promiseWriteWebManifestForExtension(aData, aDir, aId) { + let files = { + "manifest.json": JSON.stringify(aData), + }; + if (!aId) { + aId = + aData?.browser_specific_settings?.gecko?.id || + aData?.applications?.gecko?.id; + } + return AddonTestUtils.promiseWriteFilesToExtension(aDir.path, aId, files); +} + +function hasFlag(aBits, aFlag) { + return (aBits & aFlag) != 0; +} + +class EventChecker { + constructor(options) { + this.expectedEvents = options.addonEvents || {}; + this.expectedInstalls = options.installEvents || null; + this.ignorePlugins = options.ignorePlugins || false; + + this.finished = new Promise(resolve => { + this.resolveFinished = resolve; + }); + + AddonManager.addAddonListener(this); + if (this.expectedInstalls) { + AddonManager.addInstallListener(this); + } + } + + cleanup() { + AddonManager.removeAddonListener(this); + if (this.expectedInstalls) { + AddonManager.removeInstallListener(this); + } + } + + checkValue(prop, value, flagName) { + if (Array.isArray(flagName)) { + let names = flagName.map(name => `AddonManager.${name}`); + + Assert.ok( + flagName.map(name => AddonManager[name]).includes(value), + `${prop} value \`${value}\` should be one of [${names.join(", ")}` + ); + } else { + Assert.equal( + value, + AddonManager[flagName], + `${prop} should have value AddonManager.${flagName}` + ); + } + } + + checkFlag(prop, value, flagName) { + Assert.equal( + value & AddonManager[flagName], + AddonManager[flagName], + `${prop} should have flag AddonManager.${flagName}` + ); + } + + checkNoFlag(prop, value, flagName) { + Assert.ok( + !(value & AddonManager[flagName]), + `${prop} should not have flag AddonManager.${flagName}` + ); + } + + checkComplete() { + if (this.expectedInstalls && this.expectedInstalls.length) { + return; + } + + if (Object.values(this.expectedEvents).some(events => events.length)) { + return; + } + + info("Test complete"); + this.cleanup(); + this.resolveFinished(); + } + + ensureComplete() { + this.cleanup(); + + for (let [id, events] of Object.entries(this.expectedEvents)) { + Assert.equal( + events.length, + 0, + `Should have no remaining events for ${id}` + ); + } + if (this.expectedInstalls) { + Assert.deepEqual( + this.expectedInstalls, + [], + "Should have no remaining install events" + ); + } + } + + // Add-on listener events + getExpectedEvent(aId) { + if (!(aId in this.expectedEvents)) { + return null; + } + + let events = this.expectedEvents[aId]; + Assert.ok(!!events.length, `Should be expecting events for ${aId}`); + + return events.shift(); + } + + checkAddonEvent(event, addon, details = {}) { + info(`Got event "${event}" for add-on ${addon.id}`); + + if ("requiresRestart" in details) { + Assert.equal( + details.requiresRestart, + false, + "requiresRestart should always be false" + ); + } + + let expected = this.getExpectedEvent(addon.id); + if (!expected) { + return undefined; + } + + Assert.equal( + expected.event, + event, + `Expecting event "${expected.event}" got "${event}"` + ); + + for (let prop of ["properties"]) { + if (prop in expected) { + Assert.deepEqual( + expected[prop], + details[prop], + `Expected value for ${prop}` + ); + } + } + + this.checkComplete(); + + if ("returnValue" in expected) { + return expected.returnValue; + } + return undefined; + } + + onPropertyChanged(addon, properties) { + return this.checkAddonEvent("onPropertyChanged", addon, { properties }); + } + + onEnabling(addon, requiresRestart) { + let result = this.checkAddonEvent("onEnabling", addon, { requiresRestart }); + + this.checkNoFlag("addon.permissions", addon.permissions, "PERM_CAN_ENABLE"); + + return result; + } + + onEnabled(addon) { + let result = this.checkAddonEvent("onEnabled", addon); + + this.checkNoFlag("addon.permissions", addon.permissions, "PERM_CAN_ENABLE"); + + return result; + } + + onDisabling(addon, requiresRestart) { + let result = this.checkAddonEvent("onDisabling", addon, { + requiresRestart, + }); + + this.checkNoFlag( + "addon.permissions", + addon.permissions, + "PERM_CAN_DISABLE" + ); + return result; + } + + onDisabled(addon) { + let result = this.checkAddonEvent("onDisabled", addon); + + this.checkNoFlag( + "addon.permissions", + addon.permissions, + "PERM_CAN_DISABLE" + ); + + return result; + } + + onInstalling(addon, requiresRestart) { + return this.checkAddonEvent("onInstalling", addon, { requiresRestart }); + } + + onInstalled(addon) { + return this.checkAddonEvent("onInstalled", addon); + } + + onUninstalling(addon, requiresRestart) { + return this.checkAddonEvent("onUninstalling", addon); + } + + onUninstalled(addon) { + return this.checkAddonEvent("onUninstalled", addon); + } + + onOperationCancelled(addon) { + return this.checkAddonEvent("onOperationCancelled", addon); + } + + // Install listener events. + checkInstall(event, install, details = {}) { + // Lazy initialization of the plugin host means we can get spurious + // install events for plugins. If we're not looking for plugin + // installs, ignore them completely. If we *are* looking for plugin + // installs, the onus is on the individual test to ensure it waits + // for the plugin host to have done its initial work. + if (this.ignorePlugins && install.type == "plugin") { + info(`Ignoring install event for plugin ${install.id}`); + return undefined; + } + info(`Got install event "${event}"`); + + let expected = this.expectedInstalls.shift(); + Assert.ok(expected, "Should be expecting install event"); + + Assert.equal( + expected.event, + event, + "Should be expecting onExternalInstall event" + ); + + if ("state" in details) { + this.checkValue("install.state", install.state, details.state); + } + + this.checkComplete(); + + if ("callback" in expected) { + expected.callback(install); + } + + if ("returnValue" in expected) { + return expected.returnValue; + } + return undefined; + } + + onNewInstall(install) { + let result = this.checkInstall("onNewInstall", install, { + state: ["STATE_DOWNLOADED", "STATE_DOWNLOAD_FAILED", "STATE_AVAILABLE"], + }); + + if (install.state != AddonManager.STATE_DOWNLOAD_FAILED) { + Assert.equal(install.error, 0, "Should have no error"); + } else { + Assert.notEqual(install.error, 0, "Should have error"); + } + + return result; + } + + onDownloadStarted(install) { + return this.checkInstall("onDownloadStarted", install, { + state: "STATE_DOWNLOADING", + error: 0, + }); + } + + onDownloadEnded(install) { + return this.checkInstall("onDownloadEnded", install, { + state: "STATE_DOWNLOADED", + error: 0, + }); + } + + onDownloadFailed(install) { + return this.checkInstall("onDownloadFailed", install, { + state: "STATE_FAILED", + }); + } + + onDownloadCancelled(install) { + return this.checkInstall("onDownloadCancelled", install, { + state: "STATE_CANCELLED", + error: 0, + }); + } + + onInstallStarted(install) { + return this.checkInstall("onInstallStarted", install, { + state: "STATE_INSTALLING", + error: 0, + }); + } + + onInstallEnded(install, newAddon) { + return this.checkInstall("onInstallEnded", install, { + state: "STATE_INSTALLED", + error: 0, + }); + } + + onInstallFailed(install) { + return this.checkInstall("onInstallFailed", install, { + state: "STATE_FAILED", + }); + } + + onInstallCancelled(install) { + // If the install was cancelled by a listener returning false from + // onInstallStarted, then the state will revert to STATE_DOWNLOADED. + return this.checkInstall("onInstallCancelled", install, { + state: ["STATE_CANCELED", "STATE_DOWNLOADED"], + error: 0, + }); + } + + onExternalInstall(addon, existingAddon, requiresRestart) { + if (this.ignorePlugins && addon.type == "plugin") { + info(`Ignoring install event for plugin ${addon.id}`); + return undefined; + } + let expected = this.expectedInstalls.shift(); + Assert.ok(expected, "Should be expecting install event"); + + Assert.equal( + expected.event, + "onExternalInstall", + "Should be expecting onExternalInstall event" + ); + Assert.ok(!requiresRestart, "Should never require restart"); + + this.checkComplete(); + if ("returnValue" in expected) { + return expected.returnValue; + } + return undefined; + } +} + +/** + * Run the giving callback function, and expect the given set of add-on + * and install listener events to be emitted, and returns a promise + * which resolves when they have all been observed. + * + * If `callback` returns a promise, all events are expected to be + * observed by the time the promise resolves. If not, simply waits for + * all events to be observed before resolving the returned promise. + * + * @param {object} details + * @param {function} callback + * @returns {Promise} + */ +/* exported expectEvents */ +async function expectEvents(details, callback) { + let checker = new EventChecker(details); + + try { + let result = callback(); + + if ( + result && + typeof result === "object" && + typeof result.then === "function" + ) { + result = await result; + checker.ensureComplete(); + } else { + await checker.finished; + } + + return result; + } catch (e) { + do_throw(e); + return undefined; + } +} + +const EXTENSIONS_DB = "extensions.json"; +var gExtensionsJSON = gProfD.clone(); +gExtensionsJSON.append(EXTENSIONS_DB); + +async function promiseInstallWebExtension(aData) { + let addonFile = createTempWebExtensionFile(aData); + + let { addon } = await promiseInstallFile(addonFile); + return addon; +} + +// By default use strict compatibility +Services.prefs.setBoolPref("extensions.strictCompatibility", true); + +// Ensure signature checks are enabled by default +Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true); + +Services.prefs.setBoolPref("extensions.experiments.enabled", true); + +// Copies blocklistFile (an nsIFile) to gProfD/blocklist.xml. +function copyBlocklistToProfile(blocklistFile) { + var dest = gProfD.clone(); + dest.append("blocklist.xml"); + if (dest.exists()) { + dest.remove(false); + } + blocklistFile.copyTo(gProfD, "blocklist.xml"); + dest.lastModifiedTime = Date.now(); +} + +async function mockGfxBlocklistItemsFromDisk(path) { + let response = await fetch(Services.io.newFileURI(do_get_file(path)).spec); + let json = await response.json(); + return mockGfxBlocklistItems(json); +} + +async function mockGfxBlocklistItems(items) { + const { generateUUID } = Services.uuid; + const { BlocklistPrivate } = ChromeUtils.importESModule( + "resource://gre/modules/Blocklist.sys.mjs" + ); + const client = RemoteSettings("gfx", { + bucketName: "blocklists", + }); + const records = items.map(item => { + if (item.id && item.last_modified) { + return item; + } + return { + id: generateUUID().toString().replace(/[{}]/g, ""), + last_modified: Date.now(), + ...item, + }; + }); + const collectionTimestamp = Math.max(...records.map(r => r.last_modified)); + await client.db.importChanges({}, collectionTimestamp, records, { + clear: true, + }); + let rv = await BlocklistPrivate.GfxBlocklistRS.checkForEntries(); + return rv; +} + +/** + * Change the schema version of the JSON extensions database + */ +async function changeXPIDBVersion(aNewVersion) { + let json = await IOUtils.readJSON(gExtensionsJSON.path); + json.schemaVersion = aNewVersion; + await IOUtils.writeJSON(gExtensionsJSON.path, json); +} + +async function setInitialState(addon, initialState) { + if (initialState.userDisabled) { + await addon.disable(); + } else if (initialState.userDisabled === false) { + await addon.enable(); + } +} + +async function setupBuiltinExtension(extensionData, location = "ext-test") { + let xpi = await AddonTestUtils.createTempWebExtensionFile(extensionData); + + // The built-in location requires a resource: URL that maps to a + // jar: or file: URL. This would typically be something bundled + // into omni.ja but for testing we just use a temp file. + let base = Services.io.newURI(`jar:file:${xpi.path}!/`); + let resProto = Services.io + .getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + resProto.setSubstitution(location, base); +} + +async function installBuiltinExtension(extensionData, waitForStartup = true) { + await setupBuiltinExtension(extensionData); + + let id = + extensionData.manifest?.browser_specific_settings?.gecko?.id || + extensionData.manifest?.applications?.gecko?.id; + let wrapper = ExtensionTestUtils.expectExtension(id); + await AddonManager.installBuiltinAddon("resource://ext-test/"); + if (waitForStartup) { + await wrapper.awaitStartup(); + } + return wrapper; +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_amremotesettings.js b/toolkit/mozapps/extensions/test/xpcshell/head_amremotesettings.js new file mode 100644 index 0000000000..36741736fa --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/head_amremotesettings.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.defineESModuleGetters(this, { + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", +}); + +async function setAndEmitFakeRemoteSettingsData( + data, + expectClientInitialized = true +) { + const { AMRemoteSettings } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" + ); + let client; + if (expectClientInitialized) { + ok(AMRemoteSettings.client, "Got a remote settings client"); + ok(AMRemoteSettings.onSync, "Got a remote settings 'sync' event handler"); + client = AMRemoteSettings.client; + } else { + // No client is expected to exist, and so we create one to inject the expected data + // into the RemoteSettings db. + client = new RemoteSettings(AMRemoteSettings.RS_COLLECTION); + } + + await client.db.clear(); + if (data.length) { + await client.db.importChanges({}, Date.now(), data); + } + await client.emit("sync", { data: {} }); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_cert_handling.js b/toolkit/mozapps/extensions/test/xpcshell/head_cert_handling.js new file mode 100644 index 0000000000..08c41a8c7e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/head_cert_handling.js @@ -0,0 +1,33 @@ +// Helpers for handling certs. +// These are taken from +// https://searchfox.org/mozilla-central/rev/36aa22c7ea92bd3cf7910774004fff7e63341cf5/security/manager/ssl/tests/unit/head_psm.js +// but we don't want to drag that file in here because +// - it conflicts with `head_addons.js`. +// - it has a lot of extra code we don't need. +// So dupe relevant code here. + +// This file will be included along with head_addons.js, use its globals. +/* import-globals-from head_addons.js */ + +"use strict"; + +function readFile(file) { + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(file, -1, 0, 0); + let available = fstream.available(); + let data = + available > 0 ? NetUtil.readInputStreamToString(fstream, available) : ""; + fstream.close(); + return data; +} + +function loadCertChain(prefix, names) { + let chain = []; + for (let name of names) { + let filename = `${prefix}_${name}.pem`; + chain.push(readFile(do_get_file(filename))); + } + return chain; +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_compat.js b/toolkit/mozapps/extensions/test/xpcshell/head_compat.js new file mode 100644 index 0000000000..79ddb8dd3f --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/head_compat.js @@ -0,0 +1,47 @@ +// +// This file provides helpers for tests of addons that use strictCompatibility. +// Since WebExtensions cannot opt out of strictCompatibility, we add a +// simple extension loader that lets tests directly set AddonInternal +// properties (including strictCompatibility) +// + +/* import-globals-from head_addons.js */ + +const { XPIExports } = ChromeUtils.importESModule( + "resource://gre/modules/addons/XPIExports.sys.mjs" +); + +const MANIFEST = "compat_manifest.json"; + +AddonManager.addExternalExtensionLoader({ + name: "compat-test", + manifestFile: MANIFEST, + async loadManifest(pkg) { + let addon = new XPIExports.AddonInternal(); + let manifest = JSON.parse(await pkg.readString(MANIFEST)); + Object.assign(addon, manifest); + return addon; + }, + loadScope(addon, file) { + return { + install() {}, + uninstall() {}, + startup() {}, + shutdonw() {}, + }; + }, +}); + +const DEFAULTS = { + defaultLocale: {}, + locales: [], + targetPlatforms: [], + type: "extension", + version: "1.0", +}; + +function createAddon(manifest) { + return AddonTestUtils.createTempXPIFile({ + [MANIFEST]: Object.assign({}, DEFAULTS, manifest), + }); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_sideload.js b/toolkit/mozapps/extensions/test/xpcshell/head_sideload.js new file mode 100644 index 0000000000..8ff3f2f072 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/head_sideload.js @@ -0,0 +1,76 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +/* import-globals-from head_addons.js */ + +// Enable all scopes. +Services.prefs.setIntPref("extensions.enabledScopes", AddonManager.SCOPE_ALL); +// Setting this to all enables the same behavior as before disabling sideloading. +// We reset this later after doing some legacy sideloading. +Services.prefs.setIntPref("extensions.sideloadScopes", AddonManager.SCOPE_ALL); +// AddonTestUtils sets this to zero, we need the default value. +Services.prefs.clearUserPref("extensions.autoDisableScopes"); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + +function getID(n) { + return `${n}@tests.mozilla.org`; +} +function initialVersion(n) { + return `${n}.0`; +} + +// Setup some common extension locations, one in each scope. + +// SCOPE_SYSTEM +const globalDir = gProfD.clone(); +globalDir.append("app-system-share"); +globalDir.append(gAppInfo.ID); +registerDirectory("XRESysSExtPD", globalDir.parent); + +// SCOPE_USER +const userDir = gProfD.clone(); +userDir.append("app-system-user"); +userDir.append(gAppInfo.ID); +registerDirectory("XREUSysExt", userDir.parent); + +// SCOPE_APPLICATION +const addonAppDir = gProfD.clone(); +addonAppDir.append("app-global"); +addonAppDir.append("extensions"); +registerDirectory("XREAddonAppDir", addonAppDir.parent); + +// SCOPE_PROFILE +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +const scopeDirectories = { + global: globalDir, + user: userDir, + app: addonAppDir, + profile: profileDir, +}; + +const scopeToDir = new Map([ + [AddonManager.SCOPE_SYSTEM, globalDir], + [AddonManager.SCOPE_USER, userDir], + [AddonManager.SCOPE_APPLICATION, addonAppDir], + [AddonManager.SCOPE_PROFILE, profileDir], +]); + +async function createWebExtension(id, version, dir) { + let xpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version, + browser_specific_settings: { gecko: { id } }, + }, + }); + await AddonTestUtils.manuallyInstall(xpi, dir); +} + +function check_startup_changes(aType, aIds) { + let changes = AddonManager.getStartupChanges(aType); + changes = changes.filter(aEl => /@tests.mozilla.org$/.test(aEl)); + + Assert.deepEqual([...aIds].sort(), changes.sort()); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_system_addons.js b/toolkit/mozapps/extensions/test/xpcshell/head_system_addons.js new file mode 100644 index 0000000000..2c77aa8019 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/head_system_addons.js @@ -0,0 +1,486 @@ +/* import-globals-from head_addons.js */ + +const PREF_SYSTEM_ADDON_SET = "extensions.systemAddonSet"; +const PREF_SYSTEM_ADDON_UPDATE_URL = "extensions.systemAddon.update.url"; +const PREF_SYSTEM_ADDON_UPDATE_ENABLED = + "extensions.systemAddon.update.enabled"; + +// See bug 1507255 +Services.prefs.setBoolPref("media.gmp-manager.updateEnabled", true); + +function root(server) { + let { primaryScheme, primaryHost, primaryPort } = server.identity; + return `${primaryScheme}://${primaryHost}:${primaryPort}/data`; +} + +ChromeUtils.defineLazyGetter(this, "testserver", () => { + let server = new HttpServer(); + server.start(); + Services.prefs.setCharPref( + PREF_SYSTEM_ADDON_UPDATE_URL, + `${root(server)}/update.xml` + ); + return server; +}); + +async function serveSystemUpdate(xml, perform_update) { + testserver.registerPathHandler("/data/update.xml", (request, response) => { + response.write(xml); + }); + + try { + await perform_update(); + } finally { + testserver.registerPathHandler("/data/update.xml", null); + } +} + +// Runs an update check making it use the passed in xml string. Uses the direct +// call to the update function so we get rejections on failure. +async function installSystemAddons(xml, waitIDs = []) { + info("Triggering system add-on update check."); + + await serveSystemUpdate( + xml, + async function () { + let { XPIExports } = ChromeUtils.importESModule( + "resource://gre/modules/addons/XPIExports.sys.mjs" + ); + await Promise.all([ + XPIExports.XPIProvider.updateSystemAddons(), + ...waitIDs.map(id => promiseWebExtensionStartup(id)), + ]); + }, + testserver + ); +} + +// Runs a full add-on update check which will in some cases do a system add-on +// update check. Always succeeds. +async function updateAllSystemAddons(xml) { + info("Triggering full add-on update check."); + + await serveSystemUpdate( + xml, + function () { + return new Promise(resolve => { + Services.obs.addObserver(function observer() { + Services.obs.removeObserver( + observer, + "addons-background-update-complete" + ); + + resolve(); + }, "addons-background-update-complete"); + + // Trigger the background update timer handler + gInternalManager.notify(null); + }); + }, + testserver + ); +} + +// Builds an update.xml file for an update check based on the data passed. +function buildSystemAddonUpdates(addons) { + let xml = `<?xml version="1.0" encoding="UTF-8"?>\n\n<updates>\n`; + if (addons) { + xml += ` <addons>\n`; + for (let addon of addons) { + if (addon.xpi) { + testserver.registerFile(`/data/${addon.path}`, addon.xpi); + } + + xml += ` <addon id="${addon.id}" URL="${root(testserver)}/${ + addon.path + }" version="${addon.version}"`; + if (addon.hashFunction) { + xml += ` hashFunction="${addon.hashFunction}"`; + } + if (addon.hashValue) { + xml += ` hashValue="${addon.hashValue}"`; + } + xml += `/>\n`; + } + xml += ` </addons>\n`; + } + xml += `</updates>\n`; + + return xml; +} + +let _systemXPIs = new Map(); +function getSystemAddonXPI(num, version) { + let key = `${num}:${version}`; + if (!_systemXPIs.has(key)) { + _systemXPIs.set( + key, + AddonTestUtils.createTempWebExtensionFile({ + manifest: { + name: `System Add-on ${num}`, + version, + browser_specific_settings: { + gecko: { + id: `system${num}@tests.mozilla.org`, + }, + }, + }, + }) + ); + } + return _systemXPIs.get(key); +} + +async function initSystemAddonDirs() { + let hiddenSystemAddonDir = FileUtils.getDir("ProfD", [ + "sysfeatures", + "hidden", + ]); + hiddenSystemAddonDir.create( + Ci.nsIFile.DIRECTORY_TYPE, + FileUtils.PERMS_DIRECTORY + ); + let system1_1 = await getSystemAddonXPI(1, "1.0"); + system1_1.copyTo(hiddenSystemAddonDir, "system1@tests.mozilla.org.xpi"); + + let system2_1 = await getSystemAddonXPI(2, "1.0"); + system2_1.copyTo(hiddenSystemAddonDir, "system2@tests.mozilla.org.xpi"); + + let prefilledSystemAddonDir = FileUtils.getDir("ProfD", [ + "sysfeatures", + "prefilled", + ]); + prefilledSystemAddonDir.create( + Ci.nsIFile.DIRECTORY_TYPE, + FileUtils.PERMS_DIRECTORY + ); + let system2_2 = await getSystemAddonXPI(2, "2.0"); + system2_2.copyTo(prefilledSystemAddonDir, "system2@tests.mozilla.org.xpi"); + let system3_2 = await getSystemAddonXPI(3, "2.0"); + system3_2.copyTo(prefilledSystemAddonDir, "system3@tests.mozilla.org.xpi"); +} + +/** + * Returns current system add-on update directory (stored in pref). + */ +function getCurrentSystemAddonUpdatesDir() { + const updatesDir = FileUtils.getDir("ProfD", ["features"]); + let dir = updatesDir.clone(); + let set = JSON.parse(Services.prefs.getCharPref(PREF_SYSTEM_ADDON_SET)); + dir.append(set.directory); + return dir; +} + +/** + * Removes all files from system add-on update directory. + */ +function clearSystemAddonUpdatesDir() { + const updatesDir = FileUtils.getDir("ProfD", ["features"]); + // Delete any existing directories + if (updatesDir.exists()) { + updatesDir.remove(true); + } + + Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_SET); +} + +registerCleanupFunction(() => { + clearSystemAddonUpdatesDir(); +}); + +/** + * Installs a known set of add-ons into the system add-on update directory. + */ +async function buildPrefilledUpdatesDir() { + clearSystemAddonUpdatesDir(); + + // Build the test set + let dir = FileUtils.getDir("ProfD", ["features", "prefilled"]); + dir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + + let xpi = await getSystemAddonXPI(2, "2.0"); + xpi.copyTo(dir, "system2@tests.mozilla.org.xpi"); + + xpi = await getSystemAddonXPI(3, "2.0"); + xpi.copyTo(dir, "system3@tests.mozilla.org.xpi"); + + // Mark these in the past so the startup file scan notices when files have changed properly + { + let toModify = await IOUtils.getFile( + PathUtils.profileDir, + "features", + "prefilled", + "system2@tests.mozilla.org.xpi" + ); + toModify.lastModifiedTime -= 10000; + + toModify = await IOUtils.getFile( + PathUtils.profileDir, + "features", + "prefilled", + "system3@tests.mozilla.org.xpi" + ); + toModify.lastModifiedTime -= 10000; + } + + Services.prefs.setCharPref( + PREF_SYSTEM_ADDON_SET, + JSON.stringify({ + schema: 1, + directory: dir.leafName, + addons: { + "system2@tests.mozilla.org": { + version: "2.0", + }, + "system3@tests.mozilla.org": { + version: "2.0", + }, + }, + }) + ); +} + +/** + * Check currently installed ssystem add-ons against a set of conditions. + * + * @param {Array<Object>} conditions - an array of objects of the form { isUpgrade: false, version: null} + * @param {nsIFile} distroDir - the system add-on distribution directory (the "features" dir in the app directory) + */ +async function checkInstalledSystemAddons(conditions, distroDir) { + for (let i = 0; i < conditions.length; i++) { + let condition = conditions[i]; + let id = "system" + (i + 1) + "@tests.mozilla.org"; + let addon = await promiseAddonByID(id); + + if (!("isUpgrade" in condition) || !("version" in condition)) { + throw Error("condition must contain isUpgrade and version"); + } + let isUpgrade = conditions[i].isUpgrade; + let version = conditions[i].version; + + let expectedDir = isUpgrade ? getCurrentSystemAddonUpdatesDir() : distroDir; + + if (version) { + info(`Checking state of add-on ${id}, expecting version ${version}`); + + // Add-on should be installed + Assert.notEqual(addon, null); + Assert.equal(addon.version, version); + Assert.ok(addon.isActive); + Assert.ok(!addon.foreignInstall); + Assert.ok(addon.hidden); + Assert.ok(addon.isSystem); + + // Verify the add-ons file is in the right place + let file = expectedDir.clone(); + file.append(id + ".xpi"); + Assert.ok(file.exists()); + Assert.ok(file.isFile()); + + let uri = addon.getResourceURI(); + if (uri instanceof Ci.nsIJARURI) { + uri = uri.JARFile; + } + + Assert.ok(uri instanceof Ci.nsIFileURL); + Assert.equal(uri.file.path, file.path); + + if (isUpgrade) { + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_SYSTEM); + } + } else { + info(`Checking state of add-on ${id}, expecting it to be missing`); + + if (isUpgrade) { + // Add-on should not be installed + Assert.equal(addon, null); + } + } + } +} + +/** + * Returns all system add-on updates directories. + */ +async function getSystemAddonDirectories() { + const updatesDir = FileUtils.getDir("ProfD", ["features"]); + let subdirs = []; + + if (await IOUtils.exists(updatesDir.path)) { + for (const child of await IOUtils.getChildren(updatesDir.path)) { + const stat = await IOUtils.stat(child); + if (stat.type === "directory") { + subdirs.push(child); + } + } + } + + return subdirs; +} + +/** + * Sets up initial system add-on update conditions. + * + * @param {Object<function, Array<Object>} setup - an object containing a setup function and an array of objects + * of the form {isUpgrade: false, version: null} + * + * @param {nsIFile} distroDir - the system add-on distribution directory (the "features" dir in the app directory) + */ +async function setupSystemAddonConditions(setup, distroDir) { + info("Clearing existing database."); + Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_SET); + distroDir.leafName = "empty"; + + let updateList = []; + await overrideBuiltIns({ system: updateList }); + await promiseStartupManager(); + await promiseShutdownManager(); + + info("Setting up conditions."); + await setup.setup(); + + if (distroDir) { + if (distroDir.path.endsWith("hidden")) { + updateList = ["system1@tests.mozilla.org", "system2@tests.mozilla.org"]; + } else if (distroDir.path.endsWith("prefilled")) { + updateList = ["system2@tests.mozilla.org", "system3@tests.mozilla.org"]; + } + } + await overrideBuiltIns({ system: updateList }); + + let startupPromises = setup.initialState.map((item, i) => + item.version + ? promiseWebExtensionStartup(`system${i + 1}@tests.mozilla.org`) + : null + ); + await Promise.all([promiseStartupManager(), ...startupPromises]); + + // Make sure the initial state is correct + info("Checking initial state."); + await checkInstalledSystemAddons(setup.initialState, distroDir); +} + +/** + * Verify state of system add-ons after installation. + * + * @param {Array<Object>} initialState - an array of objects of the form {isUpgrade: false, version: null} + * @param {Array<Object>} finalState - an array of objects of the form {isUpgrade: false, version: null} + * @param {Boolean} alreadyUpgraded - whether a restartless upgrade has already been performed. + * @param {nsIFile} distroDir - the system add-on distribution directory (the "features" dir in the app directory) + */ +async function verifySystemAddonState( + initialState, + finalState = undefined, + alreadyUpgraded = false, + distroDir +) { + let expectedDirs = 0; + + // If the initial state was using the profile set then that directory will + // still exist. + + if (initialState.some(a => a.isUpgrade)) { + expectedDirs++; + } + + if (finalState == undefined) { + finalState = initialState; + } else if (finalState.some(a => a.isUpgrade)) { + // If the new state is using the profile then that directory will exist. + expectedDirs++; + } + + // Since upgrades are restartless now, the previous update dir hasn't been removed. + if (alreadyUpgraded) { + expectedDirs++; + } + + info("Checking final state."); + + let dirs = await getSystemAddonDirectories(); + Assert.equal(dirs.length, expectedDirs); + + await checkInstalledSystemAddons(...finalState, distroDir); + + // Check that the new state is active after a restart + await promiseShutdownManager(); + + let updateList = []; + + if (distroDir) { + if (distroDir.path.endsWith("hidden")) { + updateList = ["system1@tests.mozilla.org", "system2@tests.mozilla.org"]; + } else if (distroDir.path.endsWith("prefilled")) { + updateList = ["system2@tests.mozilla.org", "system3@tests.mozilla.org"]; + } + } + await overrideBuiltIns({ system: updateList }); + await promiseStartupManager(); + await checkInstalledSystemAddons(finalState, distroDir); +} + +/** + * Run system add-on tests and compare the results against a set of expected conditions. + * + * @param {String} setupName - name of the current setup conditions. + * @param {Object<function, Array<Object>} setup - Defines the set of initial conditions to run each test against. Each should + * define the following properties: + * setup: A task to setup the profile into the initial state. + * initialState: The initial expected system add-on state after setup has run. + * @param {Array<Object>} test - The test to run. Each test must define an updateList or test. The following + * properties are used: + * updateList: The set of add-ons the server should respond with. + * test: A function to run to perform the update check (replaces + * updateList) + * fails: An optional regex property, if present the update check is expected to + * fail. + * finalState: An optional property, the expected final state of system add-ons, + * if missing the test condition's initialState is used. + * @param {nsIFile} distroDir - the system add-on distribution directory (the "features" dir in the app directory) + */ + +async function execSystemAddonTest(setupName, setup, test, distroDir) { + // Initial system addon conditions need system signature + AddonTestUtils.usePrivilegedSignatures = "system"; + await setupSystemAddonConditions(setup, distroDir); + + // The test may define what signature to use when running the test + if (test.usePrivilegedSignatures != undefined) { + AddonTestUtils.usePrivilegedSignatures = test.usePrivilegedSignatures; + } + + function runTest() { + if ("test" in test) { + return test.test(); + } + let xml = buildSystemAddonUpdates(test.updateList); + let ids = (test.updateList || []).map(item => item.id); + return installSystemAddons(xml, ids); + } + + if (test.fails) { + await Assert.rejects(runTest(), test.fails); + } else { + await runTest(); + } + + // some tests have a different expected combination of default + // and updated add-ons. + if (test.finalState && setupName in test.finalState) { + await verifySystemAddonState( + setup.initialState, + test.finalState[setupName], + false, + distroDir + ); + } else { + await verifySystemAddonState( + setup.initialState, + undefined, + false, + distroDir + ); + } + + await promiseShutdownManager(); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_unpack.js b/toolkit/mozapps/extensions/test/xpcshell/head_unpack.js new file mode 100644 index 0000000000..909dc1da2f --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/head_unpack.js @@ -0,0 +1,3 @@ +/* globals Services, TEST_UNPACKED: true */ +/* exported TEST_UNPACKED */ +TEST_UNPACKED = true; diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/head.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/head.js new file mode 100644 index 0000000000..9075126196 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/head.js @@ -0,0 +1,57 @@ +// Appease eslint. +/* import-globals-from ../head_addons.js */ + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const IS_ANDROID_BUILD = AppConstants.platform === "android"; + +const MLBF_RECORD = { + id: "A blocklist entry that refers to a MLBF file", + // Higher than any last_modified in addons-bloomfilters.json: + last_modified: Date.now(), + attachment: { + size: 32, + hash: "6af648a5d6ce6dbee99b0aab1780d24d204977a6606ad670d5372ef22fac1052", + filename: "does-not-matter.bin", + }, + attachment_type: "bloomfilter-base", + generation_time: 1577833200000, +}; + +function enable_blocklist_v2_instead_of_useMLBF() { + Blocklist.allowDeprecatedBlocklistV2 = true; + Services.prefs.setBoolPref("extensions.blocklist.useMLBF", false); + // Sanity check: blocklist v2 has been enabled. + const { BlocklistPrivate } = ChromeUtils.importESModule( + "resource://gre/modules/Blocklist.sys.mjs" + ); + Assert.equal( + Blocklist.ExtensionBlocklist, + BlocklistPrivate.ExtensionBlocklistRS, + "ExtensionBlocklistRS should have been enabled" + ); +} + +async function load_mlbf_record_as_blob() { + const url = Services.io.newFileURI( + do_get_file("../data/mlbf-blocked1-unblocked2.bin") + ).spec; + return (await fetch(url)).blob(); +} + +function getExtensionBlocklistMLBF() { + // ExtensionBlocklist.Blocklist is an ExtensionBlocklistMLBF if the useMLBF + // pref is set to true. + const { + BlocklistPrivate: { ExtensionBlocklistMLBF }, + } = ChromeUtils.importESModule("resource://gre/modules/Blocklist.sys.mjs"); + if (Blocklist.allowDeprecatedBlocklistV2) { + Assert.ok( + Services.prefs.getBoolPref("extensions.blocklist.useMLBF", false), + "blocklist.useMLBF should be true" + ); + } + return ExtensionBlocklistMLBF; +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_android_blocklist_dump.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_android_blocklist_dump.js new file mode 100644 index 0000000000..d37e1c3c64 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_android_blocklist_dump.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// A known blocked version from bug 1626602. +// Same as in test_blocklist_mlbf_dump.js. +const blockedAddon = { + id: "{6f62927a-e380-401a-8c9e-c485b7d87f0d}", + version: "9.2.0", + signedDate: new Date(1588098908496), // 2020-04-28 (dummy date) + signedState: AddonManager.SIGNEDSTATE_SIGNED, +}; + +// A known add-on that is not blocked, as of writing. It is likely not going +// to be blocked because it does not have any executable code. +// Same as in test_blocklist_mlbf_dump.js. +const nonBlockedAddon = { + id: "disable-ctrl-q-and-cmd-q@robwu.nl", + version: "1", + signedDate: new Date(1482430349000), // 2016-12-22 (actual signing time). + signedState: AddonManager.SIGNEDSTATE_SIGNED, +}; + +add_task( + async function verify_a_known_blocked_add_on_is_not_detected_as_blocked_at_first_run() { + const MLBF_LOAD_RESULTS = []; + const MLBF_LOAD_ATTEMPTS = []; + const onLoadAttempts = record => MLBF_LOAD_ATTEMPTS.push(record); + const onLoadResult = promise => MLBF_LOAD_RESULTS.push(promise); + spyOnExtensionBlocklistMLBF(onLoadAttempts, onLoadResult); + + // The addons blocklist data is not packaged and will be downloaded after install + Assert.equal( + await Blocklist.getAddonBlocklistState(blockedAddon), + Ci.nsIBlocklistService.STATE_NOT_BLOCKED, + "A known blocked add-on should not be blocked at first" + ); + + await Assert.rejects( + MLBF_LOAD_RESULTS[0], + /DownloadError: Could not download addons-mlbf.bin/, + "Should not find any packaged attachment" + ); + + MLBF_LOAD_ATTEMPTS.length = 0; + MLBF_LOAD_RESULTS.length = 0; + + Assert.equal( + await Blocklist.getAddonBlocklistState(nonBlockedAddon), + Ci.nsIBlocklistService.STATE_NOT_BLOCKED, + "A known non-blocked add-on should not be blocked" + ); + + Assert.equal( + await Blocklist.getAddonBlocklistState(blockedAddon), + Ci.nsIBlocklistService.STATE_NOT_BLOCKED, + "Blocklist is still not populated" + ); + Assert.deepEqual( + MLBF_LOAD_ATTEMPTS, + [], + "MLBF is not fetched again after the first lookup" + ); + } +); + +function spyOnExtensionBlocklistMLBF(onLoadAttempts, onLoadResult) { + const ExtensionBlocklistMLBF = getExtensionBlocklistMLBF(); + // Tapping into the internals of ExtensionBlocklistMLBF._fetchMLBF to observe + const originalFetchMLBF = ExtensionBlocklistMLBF._fetchMLBF; + ExtensionBlocklistMLBF._fetchMLBF = async function (record) { + onLoadAttempts(record); + let promise = originalFetchMLBF.apply(this, arguments); + onLoadResult(promise); + return promise; + }; + + registerCleanupFunction( + () => (ExtensionBlocklistMLBF._fetchMLBF = originalFetchMLBF) + ); + + return ExtensionBlocklistMLBF; +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_addonBlockURL.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_addonBlockURL.js new file mode 100644 index 0000000000..b11d1329cd --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_addonBlockURL.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// useMLBF=true case is covered by test_blocklist_mlbf.js +enable_blocklist_v2_instead_of_useMLBF(); + +const BLOCKLIST_DATA = [ + { + id: "foo", + guid: "myfoo", + versionRange: [ + { + severity: "3", + }, + ], + }, + { + blockID: "bar", + // we'll get a uuid as an `id` property from loadBlocklistRawData + guid: "mybar", + versionRange: [ + { + severity: "3", + }, + ], + }, +]; + +const BASE_BLOCKLIST_INFOURL = Services.prefs.getStringPref( + "extensions.blocklist.detailsURL" +); + +/* + * Check that add-on blocklist URLs are correctly exposed + * based on either blockID or id properties on the entries + * in remote settings. + */ +add_task(async function blocklistURL_check() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + await promiseStartupManager(); + await AddonTestUtils.loadBlocklistRawData({ extensions: BLOCKLIST_DATA }); + + let entry = await Blocklist.getAddonBlocklistEntry({ + id: "myfoo", + version: "1.0", + }); + Assert.equal(entry.url, BASE_BLOCKLIST_INFOURL + "foo.html"); + + entry = await Blocklist.getAddonBlocklistEntry({ + id: "mybar", + version: "1.0", + }); + Assert.equal(entry.url, BASE_BLOCKLIST_INFOURL + "bar.html"); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_appversion.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_appversion.js new file mode 100644 index 0000000000..e8d03f088b --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_appversion.js @@ -0,0 +1,293 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +// useMLBF=true does not offer special support for filtering by application ID. +// The same functionality is offered through filter_expression, which is tested +// by services/settings/test/unit/test_remote_settings_jexl_filters.js and +// test_blocklistchange.js. +enable_blocklist_v2_instead_of_useMLBF(); + +var ADDONS = [ + { + id: "test_bug449027_1@tests.mozilla.org", + name: "Bug 449027 Addon Test 1", + version: "5", + start: false, + appBlocks: false, + toolkitBlocks: false, + }, + { + id: "test_bug449027_2@tests.mozilla.org", + name: "Bug 449027 Addon Test 2", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: false, + }, + { + id: "test_bug449027_3@tests.mozilla.org", + name: "Bug 449027 Addon Test 3", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: false, + }, + { + id: "test_bug449027_4@tests.mozilla.org", + name: "Bug 449027 Addon Test 4", + version: "5", + start: false, + appBlocks: false, + toolkitBlocks: false, + }, + { + id: "test_bug449027_5@tests.mozilla.org", + name: "Bug 449027 Addon Test 5", + version: "5", + start: false, + appBlocks: false, + toolkitBlocks: false, + }, + { + id: "test_bug449027_6@tests.mozilla.org", + name: "Bug 449027 Addon Test 6", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: false, + }, + { + id: "test_bug449027_7@tests.mozilla.org", + name: "Bug 449027 Addon Test 7", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: false, + }, + { + id: "test_bug449027_8@tests.mozilla.org", + name: "Bug 449027 Addon Test 8", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: false, + }, + { + id: "test_bug449027_9@tests.mozilla.org", + name: "Bug 449027 Addon Test 9", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: false, + }, + { + id: "test_bug449027_10@tests.mozilla.org", + name: "Bug 449027 Addon Test 10", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: false, + }, + { + id: "test_bug449027_11@tests.mozilla.org", + name: "Bug 449027 Addon Test 11", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: false, + }, + { + id: "test_bug449027_12@tests.mozilla.org", + name: "Bug 449027 Addon Test 12", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: false, + }, + { + id: "test_bug449027_13@tests.mozilla.org", + name: "Bug 449027 Addon Test 13", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: false, + }, + { + id: "test_bug449027_14@tests.mozilla.org", + name: "Bug 449027 Addon Test 14", + version: "5", + start: false, + appBlocks: false, + toolkitBlocks: false, + }, + { + id: "test_bug449027_15@tests.mozilla.org", + name: "Bug 449027 Addon Test 15", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: true, + }, + { + id: "test_bug449027_16@tests.mozilla.org", + name: "Bug 449027 Addon Test 16", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: true, + }, + { + id: "test_bug449027_17@tests.mozilla.org", + name: "Bug 449027 Addon Test 17", + version: "5", + start: false, + appBlocks: false, + toolkitBlocks: false, + }, + { + id: "test_bug449027_18@tests.mozilla.org", + name: "Bug 449027 Addon Test 18", + version: "5", + start: false, + appBlocks: false, + toolkitBlocks: false, + }, + { + id: "test_bug449027_19@tests.mozilla.org", + name: "Bug 449027 Addon Test 19", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: true, + }, + { + id: "test_bug449027_20@tests.mozilla.org", + name: "Bug 449027 Addon Test 20", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: true, + }, + { + id: "test_bug449027_21@tests.mozilla.org", + name: "Bug 449027 Addon Test 21", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: true, + }, + { + id: "test_bug449027_22@tests.mozilla.org", + name: "Bug 449027 Addon Test 22", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: true, + }, + { + id: "test_bug449027_23@tests.mozilla.org", + name: "Bug 449027 Addon Test 23", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: true, + }, + { + id: "test_bug449027_24@tests.mozilla.org", + name: "Bug 449027 Addon Test 24", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: true, + }, + { + id: "test_bug449027_25@tests.mozilla.org", + name: "Bug 449027 Addon Test 25", + version: "5", + start: false, + appBlocks: true, + toolkitBlocks: true, + }, +]; + +function createAddon(addon) { + return promiseInstallWebExtension({ + manifest: { + name: addon.name, + version: addon.version, + browser_specific_settings: { gecko: { id: addon.id } }, + }, + }); +} + +/** + * Checks that items are blocklisted correctly according to the current test. + * If a lastTest is provided checks that the notification dialog got passed + * the newly blocked items compared to the previous test. + */ +async function checkState(test, lastTest, callback) { + let addons = await AddonManager.getAddonsByIDs(ADDONS.map(a => a.id)); + + const bls = Ci.nsIBlocklistService; + + await TestUtils.waitForCondition(() => + ADDONS.every( + (addon, i) => + addon[test] == (addons[i].blocklistState == bls.STATE_BLOCKED) + ) + ).catch(() => { + /* ignore exceptions; the following test will fail anyway. */ + }); + + for (let [i, addon] of ADDONS.entries()) { + var blocked = + addons[i].blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED; + equal( + blocked, + addon[test], + `Blocklist state should match expected for extension ${addon.id}, test ${test}` + ); + } +} + +add_task(async function test() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8"); + await promiseStartupManager(); + + for (let addon of ADDONS) { + await createAddon(addon); + } + + let addons = await AddonManager.getAddonsByIDs(ADDONS.map(a => a.id)); + for (var i = 0; i < ADDONS.length; i++) { + ok(addons[i], `Addon ${i + 1} should have been correctly installed`); + } + + await checkState("start"); +}); + +/** + * Load the toolkit based blocks + */ +add_task(async function test_pt2() { + await AddonTestUtils.loadBlocklistData( + do_get_file("../data/"), + "test_bug449027_toolkit" + ); + + await checkState("toolkitBlocks", "start"); +}); + +/** + * Load the application based blocks + */ +add_task(async function test_pt3() { + await AddonTestUtils.loadBlocklistData( + do_get_file("../data/"), + "test_bug449027_app" + ); + + await checkState("appBlocks", "toolkitBlocks"); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_clients.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_clients.js new file mode 100644 index 0000000000..52d297cbf7 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_clients.js @@ -0,0 +1,225 @@ +const { BlocklistPrivate } = ChromeUtils.importESModule( + "resource://gre/modules/Blocklist.sys.mjs" +); +const { Utils: RemoteSettingsUtils } = ChromeUtils.importESModule( + "resource://services-settings/Utils.sys.mjs" +); +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); +let gBlocklistClients; + +async function clear_state() { + RemoteSettings.enablePreviewMode(undefined); + + for (let { client } of gBlocklistClients) { + // Remove last server times. + Services.prefs.clearUserPref(client.lastCheckTimePref); + + // Clear local DB. + await client.db.clear(); + } +} + +add_task(async function setup() { + AddonTestUtils.createAppInfo( + "XPCShell", + "xpcshell@tests.mozilla.org", + "1", + "" + ); + + // This will initialize the remote settings clients for blocklists. + BlocklistPrivate.ExtensionBlocklistRS.ensureInitialized(); + BlocklistPrivate.GfxBlocklistRS._ensureInitialized(); + + // ExtensionBlocklistMLBF is covered by test_blocklist_mlbf_dump.js. + gBlocklistClients = [ + { + client: BlocklistPrivate.ExtensionBlocklistRS._client, + expectHasDump: false, + }, + { + client: BlocklistPrivate.GfxBlocklistRS._client, + expectHasDump: true, + }, + ]; + + await promiseStartupManager(); +}); + +add_task( + async function test_initial_dump_is_loaded_as_synced_when_collection_is_empty() { + for (let { client, expectHasDump } of gBlocklistClients) { + Assert.equal( + await RemoteSettingsUtils.hasLocalDump( + client.bucketName, + client.collectionName + ), + expectHasDump, + `Expected initial remote settings dump for ${client.collectionName}` + ); + } + } +); +add_task(clear_state); + +add_task(async function test_data_is_filtered_for_target() { + const initial = [ + { + guid: "foo", + matchName: "foo", + versionRange: [ + { + targetApplication: [], + maxVersion: "*", + minVersion: "0", + severity: "1", + }, + ], + }, + ]; + const noMatchingTarget = [ + { + guid: "foo", + matchName: "foo", + versionRange: [ + { + targetApplication: [{ guid: "Foo" }], + maxVersion: "*", + minVersion: "0", + severity: "3", + }, + ], + }, + { + guid: "foo", + matchName: "foo", + versionRange: [ + { + targetApplication: [{ guid: "XPCShell", maxVersion: "0.1" }], + maxVersion: "*", + minVersion: "0", + severity: "1", + }, + ], + }, + ]; + const oneMatch = [ + { + guid: "foo", + matchName: "foo", + versionRange: [ + { + targetApplication: [ + { + guid: "XPCShell", + }, + ], + }, + ], + }, + ]; + + const records = initial.concat(noMatchingTarget).concat(oneMatch); + + for (let { client } of gBlocklistClients) { + // Initialize the collection with some data + for (const record of records) { + await client.db.create(record); + } + + const internalData = await client.db.list(); + Assert.equal(internalData.length, records.length); + let filtered = await client.get({ syncIfEmpty: false }); + Assert.equal(filtered.length, 2); // only two matches. + } +}); +add_task(clear_state); + +add_task( + async function test_entries_are_filtered_when_jexl_filter_expression_is_present() { + const records = [ + { + guid: "foo", + matchName: "foo", + willMatch: true, + }, + { + guid: "foo", + matchName: "foo", + willMatch: true, + filter_expression: null, + }, + { + guid: "foo", + matchName: "foo", + willMatch: true, + filter_expression: "1 == 1", + }, + { + guid: "foo", + matchName: "foo", + willMatch: false, + filter_expression: "1 == 2", + }, + { + guid: "foo", + matchName: "foo", + willMatch: true, + filter_expression: "1 == 1", + versionRange: [ + { + targetApplication: [ + { + guid: "some-guid", + }, + ], + }, + ], + }, + { + guid: "foo", + matchName: "foo", + willMatch: false, // jexl prevails over versionRange. + filter_expression: "1 == 2", + versionRange: [ + { + targetApplication: [ + { + guid: "xpcshell@tests.mozilla.org", + minVersion: "0", + maxVersion: "*", + }, + ], + }, + ], + }, + ]; + for (let { client } of gBlocklistClients) { + for (const record of records) { + await client.db.create(record); + } + const list = await client.get({ + loadDumpIfNewer: false, + syncIfEmpty: false, + }); + equal(list.length, 4); + ok(list.every(e => e.willMatch)); + } + } +); +add_task(clear_state); + +add_task(async function test_bucketname_changes_when_preview_mode_is_enabled() { + for (const { client } of gBlocklistClients) { + equal(client.bucketName, "blocklists"); + } + + RemoteSettings.enablePreviewMode(true); + + for (const { client } of gBlocklistClients) { + equal(client.bucketName, "blocklists-preview", client.identifier); + } +}); +add_task(clear_state); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_gfx.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_gfx.js new file mode 100644 index 0000000000..2b243ec650 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_gfx.js @@ -0,0 +1,113 @@ +const EVENT_NAME = "blocklist-data-gfxItems"; + +const SAMPLE_GFX_RECORD = { + driverVersionComparator: "LESS_THAN_OR_EQUAL", + driverVersion: "8.17.12.5896", + vendor: "0x10de", + blockID: "g36", + feature: "DIRECT3D_9_LAYERS", + devices: ["0x0a6c", "geforce"], + featureStatus: "BLOCKED_DRIVER_VERSION", + last_modified: 9999999999999, // High timestamp to prevent load of dump + os: "WINNT 6.1", + id: "3f947f16-37c2-4e96-d356-78b26363729b", + versionRange: { minVersion: 0, maxVersion: "*" }, +}; + +add_task(async function test_sends_serialized_data() { + const expected = + "blockID:g36\tdevices:0x0a6c,geforce\tdriverVersion:8.17.12.5896\t" + + "driverVersionComparator:LESS_THAN_OR_EQUAL\tfeature:DIRECT3D_9_LAYERS\t" + + "featureStatus:BLOCKED_DRIVER_VERSION\tos:WINNT 6.1\tvendor:0x10de\t" + + "versionRange:0,*"; + let received; + const observe = (subject, topic, data) => { + received = data; + }; + Services.obs.addObserver(observe, EVENT_NAME); + await mockGfxBlocklistItems([SAMPLE_GFX_RECORD]); + Services.obs.removeObserver(observe, EVENT_NAME); + + equal(received, expected); +}); + +add_task(async function test_parsing_skips_devices_with_comma() { + let clonedItem = Cu.cloneInto(SAMPLE_GFX_RECORD, this); + clonedItem.devices[0] = "0x2,582"; + let rv = await mockGfxBlocklistItems([clonedItem]); + equal(rv[0].devices.length, 1); + equal(rv[0].devices[0], "geforce"); +}); + +add_task(async function test_empty_values_are_ignored() { + let received; + const observe = (subject, topic, data) => { + received = data; + }; + Services.obs.addObserver(observe, EVENT_NAME); + let clonedItem = Cu.cloneInto(SAMPLE_GFX_RECORD, this); + clonedItem.os = ""; + await mockGfxBlocklistItems([clonedItem]); + ok(!received.includes("os"), "Shouldn't send empty values"); + Services.obs.removeObserver(observe, EVENT_NAME); +}); + +add_task(async function test_empty_devices_are_ignored() { + let received; + const observe = (subject, topic, data) => { + received = data; + }; + Services.obs.addObserver(observe, EVENT_NAME); + let clonedItem = Cu.cloneInto(SAMPLE_GFX_RECORD, this); + clonedItem.devices = []; + await mockGfxBlocklistItems([clonedItem]); + ok(!received.includes("devices"), "Shouldn't send empty values"); + Services.obs.removeObserver(observe, EVENT_NAME); +}); + +add_task(async function test_version_range_default_values() { + const kTests = [ + { + input: { minVersion: "13.0b2", maxVersion: "42.0" }, + output: { minVersion: "13.0b2", maxVersion: "42.0" }, + }, + { + input: { maxVersion: "2.0" }, + output: { minVersion: "0", maxVersion: "2.0" }, + }, + { + input: { minVersion: "1.0" }, + output: { minVersion: "1.0", maxVersion: "*" }, + }, + { + input: { minVersion: " " }, + output: { minVersion: "0", maxVersion: "*" }, + }, + { + input: {}, + output: { minVersion: "0", maxVersion: "*" }, + }, + ]; + for (let test of kTests) { + let parsedEntries = await mockGfxBlocklistItems([ + { versionRange: test.input }, + ]); + equal(parsedEntries[0].versionRange.minVersion, test.output.minVersion); + equal(parsedEntries[0].versionRange.maxVersion, test.output.maxVersion); + } +}); + +add_task(async function test_blockid_attribute() { + const kTests = [ + { blockID: "g60", vendor: " 0x10de " }, + { feature: " DIRECT3D_9_LAYERS " }, + ]; + for (let test of kTests) { + let [rv] = await mockGfxBlocklistItems([test]); + if (test.blockID) { + equal(rv.blockID, test.blockID); + } else { + ok(!rv.hasOwnProperty("blockID"), "not expecting a blockID"); + } + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_metadata_filters.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_metadata_filters.js new file mode 100644 index 0000000000..8f7ecbdf29 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_metadata_filters.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests blocking of extensions by ID, name, creator, homepageURL, updateURL +// and RegExps for each. See bug 897735. + +// useMLBF=true only supports blocking by version+ID, not by other fields. +enable_blocklist_v2_instead_of_useMLBF(); + +const BLOCKLIST_DATA = { + extensions: [ + { + guid: null, + name: "/^Mozilla Corp\\.$/", + versionRange: [ + { + severity: "1", + targetApplication: [ + { + guid: "xpcshell@tests.mozilla.org", + maxVersion: "2.*", + minVersion: "1", + }, + ], + }, + ], + }, + { + guid: "/block2/", + name: "/^Moz/", + homepageURL: "/\\.dangerous\\.com/", + updateURL: "/\\.dangerous\\.com/", + versionRange: [ + { + severity: "3", + targetApplication: [ + { + guid: "xpcshell@tests.mozilla.org", + maxVersion: "2.*", + minVersion: "1", + }, + ], + }, + ], + }, + ], +}; + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + + await promiseStartupManager(); + + // Should get blocked by name + await promiseInstallWebExtension({ + manifest: { + name: "Mozilla Corp.", + version: "1.0", + browser_specific_settings: { gecko: { id: "block1@tests.mozilla.org" } }, + }, + }); + + // Should get blocked by all the attributes. + await promiseInstallWebExtension({ + manifest: { + name: "Moz-addon", + version: "1.0", + homepage_url: "https://www.extension.dangerous.com/", + browser_specific_settings: { + gecko: { + id: "block2@tests.mozilla.org", + update_url: "https://www.extension.dangerous.com/update.json", + }, + }, + }, + }); + + // Fails to get blocked because of a different ID even though other + // attributes match against a blocklist entry. + await promiseInstallWebExtension({ + manifest: { + name: "Moz-addon", + version: "1.0", + homepage_url: "https://www.extension.dangerous.com/", + browser_specific_settings: { + gecko: { + id: "block3@tests.mozilla.org", + update_url: "https://www.extension.dangerous.com/update.json", + }, + }, + }, + }); + + let [a1, a2, a3] = await AddonManager.getAddonsByIDs([ + "block1@tests.mozilla.org", + "block2@tests.mozilla.org", + "block3@tests.mozilla.org", + ]); + Assert.equal(a1.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + Assert.equal(a2.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + Assert.equal(a3.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); +}); + +add_task(async function test_blocks() { + await AddonTestUtils.loadBlocklistRawData(BLOCKLIST_DATA); + + let [a1, a2, a3] = await AddonManager.getAddonsByIDs([ + "block1@tests.mozilla.org", + "block2@tests.mozilla.org", + "block3@tests.mozilla.org", + ]); + Assert.equal(a1.blocklistState, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + Assert.equal(a2.blocklistState, Ci.nsIBlocklistService.STATE_BLOCKED); + Assert.equal(a3.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf.js new file mode 100644 index 0000000000..1f6cb3db05 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf.js @@ -0,0 +1,290 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("extensions.blocklist.useMLBF", true); + +const ExtensionBlocklistMLBF = getExtensionBlocklistMLBF(); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); +AddonTestUtils.useRealCertChecks = true; + +// A real, signed XPI for use in the test. +const SIGNED_ADDON_XPI_FILE = do_get_file("../data/webext-implicit-id.xpi"); +const SIGNED_ADDON_ID = "webext_implicit_id@tests.mozilla.org"; +const SIGNED_ADDON_VERSION = "1.0"; +const SIGNED_ADDON_KEY = `${SIGNED_ADDON_ID}:${SIGNED_ADDON_VERSION}`; +const SIGNED_ADDON_SIGN_TIME = 1459980789000; // notBefore of certificate. + +// A real, signed sitepermission XPI for use in the test. +const SIGNED_SITEPERM_XPI_FILE = do_get_file("webmidi_permission.xpi"); +const SIGNED_SITEPERM_ADDON_ID = "webmidi@test.mozilla.org"; +const SIGNED_SITEPERM_ADDON_VERSION = "1.0.2"; +const SIGNED_SITEPERM_KEY = `${SIGNED_SITEPERM_ADDON_ID}:${SIGNED_SITEPERM_ADDON_VERSION}`; +const SIGNED_SITEPERM_SIGN_TIME = 1637606460000; // notBefore of certificate. + +function mockMLBF({ blocked = [], notblocked = [], generationTime }) { + // Mock _fetchMLBF to be able to have a deterministic cascade filter. + ExtensionBlocklistMLBF._fetchMLBF = async () => { + return { + cascadeFilter: { + has(blockKey) { + if (blocked.includes(blockKey)) { + return true; + } + if (notblocked.includes(blockKey)) { + return false; + } + throw new Error(`Block entry must explicitly be listed: ${blockKey}`); + }, + }, + generationTime, + }; + }; +} + +add_task(async function setup() { + await promiseStartupManager(); + mockMLBF({}); + await AddonTestUtils.loadBlocklistRawData({ + extensionsMLBF: [MLBF_RECORD], + }); +}); + +// Checks: Initially unblocked, then blocked, then unblocked again. +add_task(async function signed_xpi_initially_unblocked() { + mockMLBF({ + blocked: [], + notblocked: [SIGNED_ADDON_KEY], + generationTime: SIGNED_ADDON_SIGN_TIME + 1, + }); + await ExtensionBlocklistMLBF._onUpdate(); + + const install = await promiseInstallFile(SIGNED_ADDON_XPI_FILE); + Assert.equal(install.error, 0, "Install should not have an error"); + + let addon = await promiseAddonByID(SIGNED_ADDON_ID); + Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + + mockMLBF({ + blocked: [SIGNED_ADDON_KEY], + notblocked: [], + generationTime: SIGNED_ADDON_SIGN_TIME + 1, + }); + await ExtensionBlocklistMLBF._onUpdate(); + Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_BLOCKED); + Assert.deepEqual( + await Blocklist.getAddonBlocklistEntry(addon), + { + state: Ci.nsIBlocklistService.STATE_BLOCKED, + url: "https://addons.mozilla.org/en-US/xpcshell/blocked-addon/webext_implicit_id@tests.mozilla.org/1.0/", + }, + "Blocked addon should have blocked entry" + ); + + mockMLBF({ + blocked: [SIGNED_ADDON_KEY], + notblocked: [], + // MLBF generationTime is older, so "blocked" entry should not apply. + generationTime: SIGNED_ADDON_SIGN_TIME - 1, + }); + await ExtensionBlocklistMLBF._onUpdate(); + Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + + await addon.uninstall(); +}); + +// Checks: Initially blocked on install, then unblocked. +add_task(async function signed_xpi_blocked_on_install() { + mockMLBF({ + blocked: [SIGNED_ADDON_KEY], + notblocked: [], + generationTime: SIGNED_ADDON_SIGN_TIME + 1, + }); + await ExtensionBlocklistMLBF._onUpdate(); + + const install = await promiseInstallFile(SIGNED_ADDON_XPI_FILE); + Assert.equal( + install.error, + AddonManager.ERROR_BLOCKLISTED, + "Install should have an error" + ); + + let addon = await promiseAddonByID(SIGNED_ADDON_ID); + Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_BLOCKED); + Assert.ok(addon.appDisabled, "Blocked add-on is disabled on install"); + + mockMLBF({ + blocked: [], + notblocked: [SIGNED_ADDON_KEY], + generationTime: SIGNED_ADDON_SIGN_TIME - 1, + }); + await ExtensionBlocklistMLBF._onUpdate(); + Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + Assert.ok(!addon.appDisabled, "Re-enabled after unblock"); + + await addon.uninstall(); +}); + +// An unsigned add-on cannot be blocked. +add_task(async function unsigned_not_blocked() { + const UNSIGNED_ADDON_ID = "not-signed@tests.mozilla.org"; + const UNSIGNED_ADDON_VERSION = "1.0"; + const UNSIGNED_ADDON_KEY = `${UNSIGNED_ADDON_ID}:${UNSIGNED_ADDON_VERSION}`; + mockMLBF({ + blocked: [UNSIGNED_ADDON_KEY], + notblocked: [], + generationTime: SIGNED_ADDON_SIGN_TIME + 1, + }); + await ExtensionBlocklistMLBF._onUpdate(); + + let unsignedAddonFile = createTempWebExtensionFile({ + manifest: { + version: UNSIGNED_ADDON_VERSION, + browser_specific_settings: { gecko: { id: UNSIGNED_ADDON_ID } }, + }, + }); + + // Unsigned add-ons can generally only be loaded as a temporary install. + let [addon] = await Promise.all([ + AddonManager.installTemporaryAddon(unsignedAddonFile), + promiseWebExtensionStartup(UNSIGNED_ADDON_ID), + ]); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_MISSING); + Assert.equal(addon.signedDate, null); + Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + Assert.equal( + await Blocklist.getAddonBlocklistState(addon), + Ci.nsIBlocklistService.STATE_NOT_BLOCKED, + "Unsigned temporary add-on is not blocked" + ); + await addon.uninstall(); +}); + +// To make sure that unsigned_not_blocked did not trivially pass, we also check +// that add-ons can actually be blocked when installed as a temporary add-on. +add_task(async function signed_temporary() { + mockMLBF({ + blocked: [SIGNED_ADDON_KEY], + notblocked: [], + generationTime: SIGNED_ADDON_SIGN_TIME + 1, + }); + await ExtensionBlocklistMLBF._onUpdate(); + + await Assert.rejects( + AddonManager.installTemporaryAddon(SIGNED_ADDON_XPI_FILE), + /Add-on webext_implicit_id@tests.mozilla.org is not compatible with application version/, + "Blocklisted add-on cannot be installed" + ); +}); + +// A privileged add-on cannot be blocked by the MLBF. +// It can still be blocked by a stash, which is tested in +// privileged_addon_blocked_by_stash in test_blocklist_mlbf_stashes.js. +add_task(async function privileged_xpi_not_blocked() { + mockMLBF({ + blocked: ["test@tests.mozilla.org:2.0"], + notblocked: [], + generationTime: 1546297200000, // 1 jan 2019 = after the cert's notBefore + }); + await ExtensionBlocklistMLBF._onUpdate(); + + const install = await promiseInstallFile( + do_get_file("../data/signing_checks/privileged.xpi") + ); + Assert.equal(install.error, 0, "Install should not have an error"); + + let addon = await promiseAddonByID("test@tests.mozilla.org"); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_PRIVILEGED); + Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + await addon.uninstall(); +}); + +// Langpacks cannot be blocked via the MLBF on Nightly. +// It can still be blocked by a stash, which is tested in +// langpack_blocked_by_stash in test_blocklist_mlbf_stashes.js. +add_task( + // We do not support langpacks on Android. + { skip_if: () => AppConstants.platform == "android" }, + async function langpack_not_blocked_on_Nightly() { + mockMLBF({ + blocked: ["langpack-klingon@firefox.mozilla.org:1.0"], + notblocked: [], + generationTime: 1546297200000, // 1 jan 2019 = after the cert's notBefore + }); + await ExtensionBlocklistMLBF._onUpdate(); + + await promiseInstallFile( + do_get_file("../data/signing_checks/langpack_signed.xpi") + ); + let addon = await promiseAddonByID("langpack-klingon@firefox.mozilla.org"); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_SIGNED); + if (AppConstants.NIGHTLY_BUILD) { + // Langpacks built for Nightly are currently signed by releng and not + // submitted to AMO, so we have to ignore the blocks of the MLBF. + Assert.equal( + addon.blocklistState, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED, + "Langpacks cannot be blocked via the MLBF" + ); + } else { + // On non-Nightly, langpacks are submitted through AMO so we will enforce + // the MLBF blocklist for them. + Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_BLOCKED); + } + await addon.uninstall(); + } +); + +// Checks: Signed sitepermission addon, initially blocked on install, then unblocked. +add_task( + // We do not support this add-on type on Android. + { skip_if: () => AppConstants.platform == "android" }, + async function signed_sitepermission_xpi_blocked_on_install() { + mockMLBF({ + blocked: [SIGNED_SITEPERM_KEY], + notblocked: [], + generationTime: SIGNED_SITEPERM_SIGN_TIME + 1, + }); + await ExtensionBlocklistMLBF._onUpdate(); + + const install = await promiseInstallFile(SIGNED_SITEPERM_XPI_FILE); + Assert.equal( + install.error, + AddonManager.ERROR_BLOCKLISTED, + "Install should have an error" + ); + + let addon = await promiseAddonByID(SIGNED_SITEPERM_ADDON_ID); + // NOTE: if this assertion fails, then SIGNED_SITEPERM_SIGN_TIME has to be + // updated accordingly otherwise the addon would not be blocked on install + // as this test expects (using the value got from `addon.signedDate.getTime()`) + equal( + addon.signedDate?.getTime(), + SIGNED_SITEPERM_SIGN_TIME, + "The addon xpi has the expected signedDate timestamp" + ); + Assert.equal( + addon.blocklistState, + Ci.nsIBlocklistService.STATE_BLOCKED, + "Got the expected STATE_BLOCKED blocklistState" + ); + Assert.ok(addon.appDisabled, "Blocked add-on is disabled on install"); + + mockMLBF({ + blocked: [], + notblocked: [SIGNED_SITEPERM_KEY], + generationTime: SIGNED_SITEPERM_SIGN_TIME - 1, + }); + await ExtensionBlocklistMLBF._onUpdate(); + Assert.equal( + addon.blocklistState, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED, + "Got the expected STATE_NOT_BLOCKED blocklistState" + ); + Assert.ok(!addon.appDisabled, "Re-enabled after unblock"); + + await addon.uninstall(); + } +); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_dump.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_dump.js new file mode 100644 index 0000000000..5ac4dc965b --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_dump.js @@ -0,0 +1,155 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * @fileOverview Verifies that the MLBF dump of the addons blocklist is + * correctly registered. + */ + +Services.prefs.setBoolPref("extensions.blocklist.useMLBF", true); + +const ExtensionBlocklistMLBF = getExtensionBlocklistMLBF(); + +// A known blocked version from bug 1626602. +const blockedAddon = { + id: "{6f62927a-e380-401a-8c9e-c485b7d87f0d}", + version: "9.2.0", + // The following date is the date of the first checked in MLBF. Any MLBF + // generated in the future should be generated after this date, to be useful. + signedDate: new Date(1588098908496), // 2020-04-28 (dummy date) + signedState: AddonManager.SIGNEDSTATE_SIGNED, +}; + +// A known add-on that is not blocked, as of writing. It is likely not going +// to be blocked because it does not have any executable code. +const nonBlockedAddon = { + id: "disable-ctrl-q-and-cmd-q@robwu.nl", + version: "1", + signedDate: new Date(1482430349000), // 2016-12-22 (actual signing time). + signedState: AddonManager.SIGNEDSTATE_SIGNED, +}; + +async function sha256(arrayBuffer) { + let hash = await crypto.subtle.digest("SHA-256", arrayBuffer); + const toHex = b => b.toString(16).padStart(2, "0"); + return Array.from(new Uint8Array(hash), toHex).join(""); +} + +// A list of { inputRecord, downloadPromise }: +// - inputRecord is the record that was used for looking up the MLBF. +// - downloadPromise is the result of trying to download it. +const observed = []; + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + ExtensionBlocklistMLBF.ensureInitialized(); + + // Tapping into the internals of ExtensionBlocklistMLBF._fetchMLBF to observe + // MLBF request details. + + // Despite being called "download", this does not actually access the network + // when there is a valid dump. + const originalImpl = ExtensionBlocklistMLBF._client.attachments.download; + ExtensionBlocklistMLBF._client.attachments.download = function (record) { + let downloadPromise = originalImpl.apply(this, arguments); + observed.push({ inputRecord: record, downloadPromise }); + return downloadPromise; + }; + + await promiseStartupManager(); +}); + +async function verifyBlocklistWorksWithDump() { + Assert.equal( + await Blocklist.getAddonBlocklistState(blockedAddon), + Ci.nsIBlocklistService.STATE_BLOCKED, + "A add-on that is known to be on the blocklist should be blocked" + ); + Assert.equal( + await Blocklist.getAddonBlocklistState(nonBlockedAddon), + Ci.nsIBlocklistService.STATE_NOT_BLOCKED, + "A known non-blocked add-on should not be blocked" + ); +} + +add_task(async function verify_dump_first_run() { + await verifyBlocklistWorksWithDump(); + Assert.equal(observed.length, 1, "expected number of MLBF download requests"); + + const { inputRecord, downloadPromise } = observed.pop(); + + Assert.ok(inputRecord, "addons-bloomfilters collection dump exists"); + + const downloadResult = await downloadPromise; + + // Verify that the "download" result really originates from the local dump. + // "dump_match" means that the record exists in the collection and that an + // attachment was found. + // + // If this fails: + // - "dump_fallback" means that the MLBF attachment is out of sync with the + // collection data. + // - undefined could mean that the implementation of Attachments.sys.mjs changed. + Assert.equal( + downloadResult._source, + "dump_match", + "MLBF attachment should match the RemoteSettings collection" + ); + + Assert.equal( + await sha256(downloadResult.buffer), + inputRecord.attachment.hash, + "The content of the attachment should actually matches the record" + ); +}); + +add_task(async function use_dump_fallback_when_collection_is_out_of_sync() { + await AddonTestUtils.loadBlocklistRawData({ + // last_modified higher than any value in addons-bloomfilters.json. + extensionsMLBF: [{ last_modified: Date.now() }], + }); + Assert.equal(observed.length, 1, "Expected new download on update"); + + const { inputRecord, downloadPromise } = observed.pop(); + Assert.equal(inputRecord, null, "No MLBF record found"); + + const downloadResult = await downloadPromise; + Assert.equal( + downloadResult._source, + "dump_fallback", + "should have used fallback despite the absence of a MLBF record" + ); + + await verifyBlocklistWorksWithDump(); + Assert.equal(observed.length, 0, "Blocklist uses cached result"); +}); + +// Verifies that the dump would supersede local data. This can happen after an +// application upgrade, where the local database contains outdated records from +// a previous application version. +add_task(async function verify_dump_supersedes_old_dump() { + // Delete in-memory value; otherwise the cached record from the previous test + // task would be re-used and nothing would be downloaded. + delete ExtensionBlocklistMLBF._mlbfData; + + await AddonTestUtils.loadBlocklistRawData({ + // last_modified lower than any value in addons-bloomfilters.json. + extensionsMLBF: [{ last_modified: 1 }], + }); + Assert.equal(observed.length, 1, "Expected new download on update"); + + const { inputRecord, downloadPromise } = observed.pop(); + Assert.ok(inputRecord, "should have read from addons-bloomfilters dump"); + + const downloadResult = await downloadPromise; + Assert.equal( + downloadResult._source, + "dump_match", + "Should have replaced outdated collection records with dump" + ); + + await verifyBlocklistWorksWithDump(); + Assert.equal(observed.length, 0, "Blocklist uses cached result"); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_fetch.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_fetch.js new file mode 100644 index 0000000000..92bf61dbde --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_fetch.js @@ -0,0 +1,231 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * @fileOverview Tests the MLBF and RemoteSettings synchronization logic. + */ + +Services.prefs.setBoolPref("extensions.blocklist.useMLBF", true); + +const { Downloader } = ChromeUtils.importESModule( + "resource://services-settings/Attachments.sys.mjs" +); + +const ExtensionBlocklistMLBF = getExtensionBlocklistMLBF(); + +// This test needs to interact with the RemoteSettings client. +ExtensionBlocklistMLBF.ensureInitialized(); + +add_task(async function fetch_invalid_mlbf_record() { + let invalidRecord = { + attachment: { size: 1, hash: "definitely not valid" }, + generation_time: 1, + }; + + // _fetchMLBF(invalidRecord) may succeed if there is a MLBF dump packaged with + // the application. This test intentionally hides the actual path to get + // deterministic results. To check whether the dump is correctly registered, + // run test_blocklist_mlbf_dump.js + + // Forget about the packaged attachment. + Downloader._RESOURCE_BASE_URL = "invalid://bogus"; + // NetworkError is expected here. The JSON.parse error could be triggered via + // _baseAttachmentsURL < downloadAsBytes < download < download < _fetchMLBF if + // the request to services.settings.server ("data:,#remote-settings-dummy/v1") + // is fulfilled (but with invalid JSON). That request is not expected to be + // fulfilled in the first place, but that is not a concern of this test. + // This test passes if _fetchMLBF() rejects when given an invalid record. + await Assert.rejects( + ExtensionBlocklistMLBF._fetchMLBF(invalidRecord), + /NetworkError|SyntaxError: JSON\.parse/, + "record not found when there is no packaged MLBF" + ); +}); + +// Other tests can mock _testMLBF, so let's verify that it works as expected. +add_task(async function fetch_valid_mlbf() { + await ExtensionBlocklistMLBF._client.db.saveAttachment( + ExtensionBlocklistMLBF.RS_ATTACHMENT_ID, + { record: MLBF_RECORD, blob: await load_mlbf_record_as_blob() } + ); + + const result = await ExtensionBlocklistMLBF._fetchMLBF(MLBF_RECORD); + Assert.equal(result.cascadeHash, MLBF_RECORD.attachment.hash, "hash OK"); + Assert.equal(result.generationTime, MLBF_RECORD.generation_time, "time OK"); + Assert.ok(result.cascadeFilter.has("@blocked:1"), "item blocked"); + Assert.ok(!result.cascadeFilter.has("@unblocked:2"), "item not blocked"); + + const result2 = await ExtensionBlocklistMLBF._fetchMLBF({ + attachment: { size: 1, hash: "invalid" }, + generation_time: Date.now(), + }); + Assert.equal( + result2.cascadeHash, + MLBF_RECORD.attachment.hash, + "The cached MLBF should be used when the attachment is invalid" + ); + + // The attachment is kept in the database for use by the next test task. +}); + +// Test that results of the public API are consistent with the MLBF file. +add_task(async function public_api_uses_mlbf() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + await promiseStartupManager(); + + const blockedAddon = { + id: "@blocked", + version: "1", + signedDate: new Date(0), // a date in the past, before MLBF's generationTime. + signedState: AddonManager.SIGNEDSTATE_SIGNED, + }; + const nonBlockedAddon = { + id: "@unblocked", + version: "2", + signedDate: new Date(0), // a date in the past, before MLBF's generationTime. + signedState: AddonManager.SIGNEDSTATE_SIGNED, + }; + + await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [MLBF_RECORD] }); + + Assert.deepEqual( + await Blocklist.getAddonBlocklistEntry(blockedAddon), + { + state: Ci.nsIBlocklistService.STATE_BLOCKED, + url: "https://addons.mozilla.org/en-US/xpcshell/blocked-addon/@blocked/1/", + }, + "Blocked addon should have blocked entry" + ); + + Assert.deepEqual( + await Blocklist.getAddonBlocklistEntry(nonBlockedAddon), + null, + "Non-blocked addon should not be blocked" + ); + + Assert.equal( + await Blocklist.getAddonBlocklistState(blockedAddon), + Ci.nsIBlocklistService.STATE_BLOCKED, + "Blocked entry should have blocked state" + ); + + Assert.equal( + await Blocklist.getAddonBlocklistState(nonBlockedAddon), + Ci.nsIBlocklistService.STATE_NOT_BLOCKED, + "Non-blocked entry should have unblocked state" + ); + + // Note: Blocklist collection and attachment carries over to the next test. +}); + +// Verifies that the metadata (time of validity) of an updated MLBF record is +// correctly used, even if the MLBF itself has not changed. +add_task(async function fetch_updated_mlbf_same_hash() { + const recordUpdate = { + ...MLBF_RECORD, + generation_time: MLBF_RECORD.generation_time + 1, + }; + const blockedAddonUpdate = { + id: "@blocked", + version: "1", + signedDate: new Date(recordUpdate.generation_time), + signedState: AddonManager.SIGNEDSTATE_SIGNED, + }; + + // The blocklist already includes "@blocked:1", but the last specified + // generation time is MLBF_RECORD.generation_time. So the addon cannot be + // blocked, because the block decision could be a false positive. + Assert.equal( + await Blocklist.getAddonBlocklistState(blockedAddonUpdate), + Ci.nsIBlocklistService.STATE_NOT_BLOCKED, + "Add-on not blocked before blocklist update" + ); + + await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [recordUpdate] }); + // The MLBF is now known to apply to |blockedAddonUpdate|. + + Assert.equal( + await Blocklist.getAddonBlocklistState(blockedAddonUpdate), + Ci.nsIBlocklistService.STATE_BLOCKED, + "Add-on blocked after update" + ); + + // Note: Blocklist collection and attachment carries over to the next test. +}); + +// Checks the remaining cases of database corruption that haven't been handled +// before. +add_task(async function handle_database_corruption() { + const blockedAddon = { + id: "@blocked", + version: "1", + signedDate: new Date(0), // a date in the past, before MLBF's generationTime. + signedState: AddonManager.SIGNEDSTATE_SIGNED, + }; + async function checkBlocklistWorks() { + Assert.equal( + await Blocklist.getAddonBlocklistState(blockedAddon), + Ci.nsIBlocklistService.STATE_BLOCKED, + "Add-on should be blocked by the blocklist" + ); + } + + let fetchCount = 0; + const originalFetchMLBF = ExtensionBlocklistMLBF._fetchMLBF; + ExtensionBlocklistMLBF._fetchMLBF = function () { + ++fetchCount; + return originalFetchMLBF.apply(this, arguments); + }; + + // In the fetch_invalid_mlbf_record we checked that a cached / packaged MLBF + // attachment is used as a fallback when the record is invalid. Here we also + // check that there is a fallback when there is no record at all. + + // Include a dummy record in the list, to prevent RemoteSettings from + // importing a JSON dump with unexpected records. + await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [{}] }); + Assert.equal(fetchCount, 1, "MLBF read once despite bad record"); + // When the collection is empty, the last known MLBF should be used anyway. + await checkBlocklistWorks(); + Assert.equal(fetchCount, 1, "MLBF not read again by blocklist query"); + + // Now we also remove the cached file... + await ExtensionBlocklistMLBF._client.db.saveAttachment( + ExtensionBlocklistMLBF.RS_ATTACHMENT_ID, + null + ); + Assert.equal(fetchCount, 1, "MLBF not read again after attachment deletion"); + // Deleting the file shouldn't cause issues because the MLBF is loaded once + // and then kept in memory. + await checkBlocklistWorks(); + Assert.equal(fetchCount, 1, "MLBF not read again by blocklist query 2"); + + // Force an update while we don't have any blocklist data nor cache. + await ExtensionBlocklistMLBF._onUpdate(); + Assert.equal(fetchCount, 2, "MLBF read again at forced update"); + // As a fallback, continue to use the in-memory version of the blocklist. + await checkBlocklistWorks(); + Assert.equal(fetchCount, 2, "MLBF not read again by blocklist query 3"); + + // Memory gone, e.g. after a browser restart. + delete ExtensionBlocklistMLBF._mlbfData; + delete ExtensionBlocklistMLBF._stashes; + Assert.equal( + await Blocklist.getAddonBlocklistState(blockedAddon), + Ci.nsIBlocklistService.STATE_NOT_BLOCKED, + "Blocklist can't work if all blocklist data is gone" + ); + Assert.equal(fetchCount, 3, "MLBF read again after restart/cleared cache"); + Assert.equal( + await Blocklist.getAddonBlocklistState(blockedAddon), + Ci.nsIBlocklistService.STATE_NOT_BLOCKED, + "Blocklist can still not work if all blocklist data is gone" + ); + // Ideally, the client packages a dump. But if the client did not package the + // dump, then it should not be trying to read the data over and over again. + Assert.equal(fetchCount, 3, "MLBF not read again despite absence of MLBF"); + + ExtensionBlocklistMLBF._fetchMLBF = originalFetchMLBF; +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_stashes.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_stashes.js new file mode 100644 index 0000000000..e129efc793 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_stashes.js @@ -0,0 +1,219 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("extensions.blocklist.useMLBF", true); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + +const ExtensionBlocklistMLBF = getExtensionBlocklistMLBF(); +const MLBF_LOAD_ATTEMPTS = []; +ExtensionBlocklistMLBF._fetchMLBF = async record => { + MLBF_LOAD_ATTEMPTS.push(record); + return { + generationTime: 0, + cascadeFilter: { + has(blockKey) { + if (blockKey === "@onlyblockedbymlbf:1") { + return true; + } + throw new Error("bloom filter should not be used in this test"); + }, + }, + }; +}; + +async function checkBlockState(addonId, version, expectBlocked) { + let addon = { + id: addonId, + version, + // Note: signedDate is missing, so the MLBF does not apply + // and we will effectively only test stashing. + }; + let state = await Blocklist.getAddonBlocklistState(addon); + if (expectBlocked) { + Assert.equal(state, Ci.nsIBlocklistService.STATE_BLOCKED); + } else { + Assert.equal(state, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + } +} + +add_task(async function setup() { + await promiseStartupManager(); +}); + +// Tests that add-ons can be blocked / unblocked via the stash. +add_task(async function basic_stash() { + await AddonTestUtils.loadBlocklistRawData({ + extensionsMLBF: [ + { + stash_time: 0, + stash: { + blocked: ["@blocked:1"], + unblocked: ["@notblocked:2"], + }, + }, + ], + }); + await checkBlockState("@blocked", "1", true); + await checkBlockState("@notblocked", "2", false); + // Not in stash (but unsigned, so shouldn't reach MLBF): + await checkBlockState("@blocked", "2", false); + + Assert.equal( + await Blocklist.getAddonBlocklistState({ + id: "@onlyblockedbymlbf", + version: "1", + signedDate: new Date(0), // = the MLBF's generationTime. + signedState: AddonManager.SIGNEDSTATE_SIGNED, + }), + Ci.nsIBlocklistService.STATE_BLOCKED, + "falls through to MLBF if entry is not found in stash" + ); + + Assert.deepEqual(MLBF_LOAD_ATTEMPTS, [null], "MLBF attachment not found"); +}); + +// To complement the privileged_xpi_not_blocked in test_blocklist_mlbf.js, +// verify that privileged add-ons can still be blocked through stashes. +add_task(async function privileged_addon_blocked_by_stash() { + const system_addon = { + id: "@blocked", + version: "1", + signedDate: new Date(0), // = the MLBF's generationTime. + signedState: AddonManager.SIGNEDSTATE_PRIVILEGED, + }; + Assert.equal( + await Blocklist.getAddonBlocklistState(system_addon), + Ci.nsIBlocklistService.STATE_BLOCKED, + "Privileged add-ons can still be blocked by a stash" + ); + + system_addon.signedState = AddonManager.SIGNEDSTATE_SYSTEM; + Assert.equal( + await Blocklist.getAddonBlocklistState(system_addon), + Ci.nsIBlocklistService.STATE_BLOCKED, + "Privileged system add-ons can still be blocked by a stash" + ); + + // For comparison, when an add-on is only blocked by a MLBF, the block + // decision is ignored. + system_addon.id = "@onlyblockedbymlbf"; + Assert.equal( + await Blocklist.getAddonBlocklistState(system_addon), + Ci.nsIBlocklistService.STATE_NOT_BLOCKED, + "Privileged add-ons cannot be blocked via a MLBF" + ); + // (note that we haven't checked that SIGNEDSTATE_PRIVILEGED is not blocked + // via the MLBF, but that is already covered by test_blocklist_mlbf.js ). +}); + +// To complement langpack_not_blocked_on_Nightly in test_blocklist_mlbf.js, +// verify that langpacks can still be blocked through stashes. +add_task(async function langpack_blocked_by_stash() { + const langpack_addon = { + id: "@blocked", + type: "locale", + version: "1", + signedDate: new Date(0), // = the MLBF's generationTime. + signedState: AddonManager.SIGNEDSTATE_SIGNED, + }; + Assert.equal( + await Blocklist.getAddonBlocklistState(langpack_addon), + Ci.nsIBlocklistService.STATE_BLOCKED, + "Langpack add-ons can still be blocked by a stash" + ); + + // For comparison, when an add-on is only blocked by a MLBF, the block + // decision is ignored on Nightly (but blocked on non-Nightly). + langpack_addon.id = "@onlyblockedbymlbf"; + if (AppConstants.NIGHTLY_BUILD) { + Assert.equal( + await Blocklist.getAddonBlocklistState(langpack_addon), + Ci.nsIBlocklistService.STATE_NOT_BLOCKED, + "Langpack add-ons cannot be blocked via a MLBF on Nightly" + ); + } else { + Assert.equal( + await Blocklist.getAddonBlocklistState(langpack_addon), + Ci.nsIBlocklistService.STATE_BLOCKED, + "Langpack add-ons can be blocked via a MLBF on non-Nightly" + ); + } +}); + +// Tests that invalid stash entries are ignored. +add_task(async function invalid_stashes() { + await AddonTestUtils.loadBlocklistRawData({ + extensionsMLBF: [ + {}, + { stash: null }, + { stash: 1 }, + { stash: {} }, + { stash: { blocked: ["@broken:1", "@okid:1"] } }, + { stash: { unblocked: ["@broken:2"] } }, + // The only correct entry: + { stash: { blocked: ["@okid:2"], unblocked: ["@okid:1"] } }, + { stash: { blocked: ["@broken:1", "@okid:1"] } }, + { stash: { unblocked: ["@broken:2", "@okid:2"] } }, + ], + }); + // The valid stash entry should be applied: + await checkBlockState("@okid", "1", false); + await checkBlockState("@okid", "2", true); + // Entries from invalid stashes should be ignored: + await checkBlockState("@broken", "1", false); + await checkBlockState("@broken", "2", false); +}); + +// Blocklist stashes should be processed in the reverse chronological order. +add_task(async function stash_time_order() { + await AddonTestUtils.loadBlocklistRawData({ + extensionsMLBF: [ + // "@a:1" and "@a:2" are blocked at time 1, but unblocked later. + { stash_time: 2, stash: { blocked: [], unblocked: ["@a:1"] } }, + { stash_time: 1, stash: { blocked: ["@a:1", "@a:2"], unblocked: [] } }, + { stash_time: 3, stash: { blocked: [], unblocked: ["@a:2"] } }, + + // "@b:1" and "@b:2" are unblocked at time 4, but blocked later. + { stash_time: 5, stash: { blocked: ["@b:1"], unblocked: [] } }, + { stash_time: 4, stash: { blocked: [], unblocked: ["@b:1", "@b:2"] } }, + { stash_time: 6, stash: { blocked: ["@b:2"], unblocked: [] } }, + ], + }); + await checkBlockState("@a", "1", false); + await checkBlockState("@a", "2", false); + + await checkBlockState("@b", "1", true); + await checkBlockState("@b", "2", true); +}); + +// Attachments with unsupported attachment_type should be ignored. +add_task(async function mlbf_bloomfilter_full_ignored() { + MLBF_LOAD_ATTEMPTS.length = 0; + + await AddonTestUtils.loadBlocklistRawData({ + extensionsMLBF: [{ attachment_type: "bloomfilter-full", attachment: {} }], + }); + + // Only bloomfilter-base records should be used. + // Since there are no such records, we shouldn't find anything. + Assert.deepEqual(MLBF_LOAD_ATTEMPTS, [null], "no matching MLBFs found"); +}); + +// Tests that the most recent MLBF is downloaded. +add_task(async function mlbf_generation_time_recent() { + MLBF_LOAD_ATTEMPTS.length = 0; + const records = [ + { attachment_type: "bloomfilter-base", attachment: {}, generation_time: 2 }, + { attachment_type: "bloomfilter-base", attachment: {}, generation_time: 3 }, + { attachment_type: "bloomfilter-base", attachment: {}, generation_time: 1 }, + ]; + await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: records }); + Assert.equal( + MLBF_LOAD_ATTEMPTS[0].generation_time, + 3, + "expected to load most recent MLBF" + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_telemetry.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_telemetry.js new file mode 100644 index 0000000000..963c6dc033 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_telemetry.js @@ -0,0 +1,188 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("extensions.blocklist.useMLBF", true); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + +const { Downloader } = ChromeUtils.importESModule( + "resource://services-settings/Attachments.sys.mjs" +); + +const { TelemetryController } = ChromeUtils.importESModule( + "resource://gre/modules/TelemetryController.sys.mjs" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const OLDEST_STASH = { stash: { blocked: [], unblocked: [] }, stash_time: 2e6 }; +const NEWEST_STASH = { stash: { blocked: [], unblocked: [] }, stash_time: 5e6 }; +const RECORDS_WITH_STASHES_AND_MLBF = [MLBF_RECORD, OLDEST_STASH, NEWEST_STASH]; + +const ExtensionBlocklistMLBF = getExtensionBlocklistMLBF(); + +function assertTelemetryScalars(expectedScalars) { + // On Android, we only report to the Glean system telemetry system. + if (IS_ANDROID_BUILD) { + info( + `Skip assertions on collected samples for ${expectedScalars} on android builds` + ); + return; + } + let scalars = TelemetryTestUtils.getProcessScalars("parent"); + for (const scalarName of Object.keys(expectedScalars || {})) { + equal( + scalars[scalarName], + expectedScalars[scalarName], + `Got the expected value for ${scalarName} scalar` + ); + } +} + +function toUTC(time) { + return new Date(time).toUTCString(); +} + +add_task(async function setup() { + if (!IS_ANDROID_BUILD) { + // FOG needs a profile directory to put its data in. + do_get_profile(); + // FOG needs to be initialized in order for data to flow. + Services.fog.initializeFOG(); + } + await TelemetryController.testSetup(); + await promiseStartupManager(); + + // Disable the packaged record and attachment to make sure that the test + // will not fall back to the packaged attachments. + Downloader._RESOURCE_BASE_URL = "invalid://bogus"; +}); + +add_task(async function test_initialization() { + Services.fog.testResetFOG(); + ExtensionBlocklistMLBF.ensureInitialized(); + + Assert.equal(undefined, Glean.blocklist.mlbfSource.testGetValue()); + Assert.equal(undefined, Glean.blocklist.mlbfGenerationTime.testGetValue()); + Assert.equal(undefined, Glean.blocklist.mlbfStashTimeOldest.testGetValue()); + Assert.equal(undefined, Glean.blocklist.mlbfStashTimeNewest.testGetValue()); + + assertTelemetryScalars({ + // In other parts of this test, this value is not checked any more. + // test_blocklist_telemetry.js already checks lastModified_rs_addons_mlbf. + "blocklist.lastModified_rs_addons_mlbf": undefined, + "blocklist.mlbf_source": undefined, + "blocklist.mlbf_generation_time": undefined, + "blocklist.mlbf_stash_time_oldest": undefined, + "blocklist.mlbf_stash_time_newest": undefined, + }); +}); + +// Test what happens if there is no blocklist data at all. +add_task(async function test_without_mlbf() { + Services.fog.testResetFOG(); + // Add one (invalid) value to the blocklist, to prevent the RemoteSettings + // client from importing the JSON dump (which could potentially cause the + // test to fail due to the unexpected imported records). + await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [{}] }); + Assert.equal("unknown", Glean.blocklist.mlbfSource.testGetValue()); + + Assert.equal(0, Glean.blocklist.mlbfGenerationTime.testGetValue().getTime()); + Assert.equal(0, Glean.blocklist.mlbfStashTimeOldest.testGetValue().getTime()); + Assert.equal(0, Glean.blocklist.mlbfStashTimeNewest.testGetValue().getTime()); + + assertTelemetryScalars({ + "blocklist.mlbf_source": "unknown", + "blocklist.mlbf_generation_time": "Missing Date", + "blocklist.mlbf_stash_time_oldest": "Missing Date", + "blocklist.mlbf_stash_time_newest": "Missing Date", + }); +}); + +// Test the telemetry that would be recorded in the common case. +add_task(async function test_common_good_case_with_stashes() { + Services.fog.testResetFOG(); + // The exact content of the attachment does not matter in this test, as long + // as the data is valid. + await ExtensionBlocklistMLBF._client.db.saveAttachment( + ExtensionBlocklistMLBF.RS_ATTACHMENT_ID, + { record: MLBF_RECORD, blob: await load_mlbf_record_as_blob() } + ); + await AddonTestUtils.loadBlocklistRawData({ + extensionsMLBF: RECORDS_WITH_STASHES_AND_MLBF, + }); + Assert.equal("cache_match", Glean.blocklist.mlbfSource.testGetValue()); + Assert.equal( + MLBF_RECORD.generation_time, + Glean.blocklist.mlbfGenerationTime.testGetValue().getTime() + ); + Assert.equal( + OLDEST_STASH.stash_time, + Glean.blocklist.mlbfStashTimeOldest.testGetValue().getTime() + ); + Assert.equal( + NEWEST_STASH.stash_time, + Glean.blocklist.mlbfStashTimeNewest.testGetValue().getTime() + ); + assertTelemetryScalars({ + "blocklist.mlbf_source": "cache_match", + "blocklist.mlbf_generation_time": toUTC(MLBF_RECORD.generation_time), + "blocklist.mlbf_stash_time_oldest": toUTC(OLDEST_STASH.stash_time), + "blocklist.mlbf_stash_time_newest": toUTC(NEWEST_STASH.stash_time), + }); + + // The records and cached attachment carries over to the next tests. +}); + +// Test what happens when there are no stashes in the collection itself. +add_task(async function test_without_stashes() { + Services.fog.testResetFOG(); + await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [MLBF_RECORD] }); + + Assert.equal("cache_match", Glean.blocklist.mlbfSource.testGetValue()); + Assert.equal( + MLBF_RECORD.generation_time, + Glean.blocklist.mlbfGenerationTime.testGetValue().getTime() + ); + + Assert.equal(0, Glean.blocklist.mlbfStashTimeOldest.testGetValue().getTime()); + Assert.equal(0, Glean.blocklist.mlbfStashTimeNewest.testGetValue().getTime()); + + assertTelemetryScalars({ + "blocklist.mlbf_source": "cache_match", + "blocklist.mlbf_generation_time": toUTC(MLBF_RECORD.generation_time), + "blocklist.mlbf_stash_time_oldest": "Missing Date", + "blocklist.mlbf_stash_time_newest": "Missing Date", + }); +}); + +// Test what happens when the collection was inadvertently emptied, +// but still with a cached mlbf from before. +add_task(async function test_without_collection_but_cache() { + Services.fog.testResetFOG(); + await AddonTestUtils.loadBlocklistRawData({ + // Insert a dummy record with a value of last_modified which is higher than + // any value of last_modified in addons-bloomfilters.json, to prevent the + // blocklist implementation from automatically falling back to the packaged + // JSON dump. + extensionsMLBF: [{ last_modified: Date.now() }], + }); + Assert.equal("cache_fallback", Glean.blocklist.mlbfSource.testGetValue()); + Assert.equal( + MLBF_RECORD.generation_time, + Glean.blocklist.mlbfGenerationTime.testGetValue().getTime() + ); + + Assert.equal(0, Glean.blocklist.mlbfStashTimeOldest.testGetValue().getTime()); + Assert.equal(0, Glean.blocklist.mlbfStashTimeNewest.testGetValue().getTime()); + + assertTelemetryScalars({ + "blocklist.mlbf_source": "cache_fallback", + "blocklist.mlbf_generation_time": toUTC(MLBF_RECORD.generation_time), + "blocklist.mlbf_stash_time_oldest": "Missing Date", + "blocklist.mlbf_stash_time_newest": "Missing Date", + }); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_update.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_update.js new file mode 100644 index 0000000000..b98d6e345d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_update.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * @fileOverview Checks that the MLBF updating logic works reasonably. + */ + +Services.prefs.setBoolPref("extensions.blocklist.useMLBF", true); +const ExtensionBlocklistMLBF = getExtensionBlocklistMLBF(); + +// This test needs to interact with the RemoteSettings client. +ExtensionBlocklistMLBF.ensureInitialized(); + +// Multiple internal calls to update should be coalesced and end up with the +// MLBF attachment from the last update call. +add_task(async function collapse_multiple_pending_update_requests() { + const observed = []; + + // The first step of starting an update is to read from the RemoteSettings + // collection. When a non-forced update is requested while another update is + // pending, the non-forced update should return/await the previous call + // instead of starting a new read/fetch from the RemoteSettings collection. + // Add a spy to the RemoteSettings client, so we can verify that the number + // of RemoteSettings accesses matches with what we expect. + const originalClientGet = ExtensionBlocklistMLBF._client.get; + const spyClientGet = (tag, returnValue) => { + ExtensionBlocklistMLBF._client.get = async function () { + // Record the method call. + observed.push(tag); + // Clone a valid record and tag it so we can identify it below. + let dummyRecord = JSON.parse(JSON.stringify(MLBF_RECORD)); + dummyRecord.tagged = tag; + return [dummyRecord]; + }; + }; + + // Another significant part of updating is fetching the MLBF attachment. + // Add a spy too, so we can check which attachment is being requested. + const originalFetchMLBF = ExtensionBlocklistMLBF._fetchMLBF; + ExtensionBlocklistMLBF._fetchMLBF = async function (record) { + observed.push(`fetchMLBF:${record.tagged}`); + throw new Error(`Deliberately ignoring call to MLBF:${record.tagged}`); + }; + + spyClientGet("initial"); // Very first call = read RS. + let update1 = ExtensionBlocklistMLBF._updateMLBF(false); + spyClientGet("unexpected update2"); // Non-forced update = reuse update1. + let update2 = ExtensionBlocklistMLBF._updateMLBF(false); + spyClientGet("forced1"); // forceUpdate=true = supersede previous update. + let forcedUpdate1 = ExtensionBlocklistMLBF._updateMLBF(true); + spyClientGet("forced2"); // forceUpdate=true = supersede previous update. + let forcedUpdate2 = ExtensionBlocklistMLBF._updateMLBF(true); + + let res = await Promise.all([update1, update2, forcedUpdate1, forcedUpdate2]); + + Assert.equal(observed.length, 4, "expected number of observed events"); + Assert.equal(observed[0], "initial", "First update should request records"); + Assert.equal(observed[1], "forced1", "Forced update supersedes initial"); + Assert.equal(observed[2], "forced2", "Forced update supersedes forced1"); + // We call the _updateMLBF methods immediately after each other. Every update + // request starts with an asynchronous operation (looking up the RS records), + // so the implementation should return early for all update requests except + // for the last one. So we should only observe a fetch for the last request. + Assert.equal(observed[3], "fetchMLBF:forced2", "expected fetch result"); + + // All update requests should end up with the same result. + Assert.equal(res[0], res[1], "update1 == update2"); + Assert.equal(res[1], res[2], "update2 == forcedUpdate1"); + Assert.equal(res[2], res[3], "forcedUpdate1 == forcedUpdate2"); + + ExtensionBlocklistMLBF._client.get = originalClientGet; + ExtensionBlocklistMLBF._fetchMLBF = originalFetchMLBF; +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_osabi.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_osabi.js new file mode 100644 index 0000000000..243225c6e0 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_osabi.js @@ -0,0 +1,286 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +// useMLBF=true only supports blocking by version+ID, not by OS/ABI. +enable_blocklist_v2_instead_of_useMLBF(); + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +const ADDONS = [ + { + id: "test_bug393285_1@tests.mozilla.org", + name: "extension 1", + version: "1.0", + + // No info in blocklist, shouldn't be blocked + notBlocklisted: [ + ["1", "1.9"], + [null, null], + ], + }, + { + id: "test_bug393285_2@tests.mozilla.org", + name: "extension 2", + version: "1.0", + + // Should always be blocked + blocklisted: [ + ["1", "1.9"], + [null, null], + ], + }, + { + id: "test_bug393285_3a@tests.mozilla.org", + name: "extension 3a", + version: "1.0", + + // Only version 1 should be blocked + blocklisted: [ + ["1", "1.9"], + [null, null], + ], + }, + { + id: "test_bug393285_3b@tests.mozilla.org", + name: "extension 3b", + version: "2.0", + + // Only version 1 should be blocked + notBlocklisted: [["1", "1.9"]], + }, + { + id: "test_bug393285_4@tests.mozilla.org", + name: "extension 4", + version: "1.0", + + // Should be blocked for app version 1 + blocklisted: [ + ["1", "1.9"], + [null, null], + ], + notBlocklisted: [["2", "1.9"]], + }, + { + id: "test_bug393285_5@tests.mozilla.org", + name: "extension 5", + version: "1.0", + + // Not blocklisted because we are a different OS + notBlocklisted: [["2", "1.9"]], + }, + { + id: "test_bug393285_6@tests.mozilla.org", + name: "extension 6", + version: "1.0", + + // Blocklisted based on OS + blocklisted: [["2", "1.9"]], + }, + { + id: "test_bug393285_7@tests.mozilla.org", + name: "extension 7", + version: "1.0", + + // Blocklisted based on OS + blocklisted: [["2", "1.9"]], + }, + { + id: "test_bug393285_8@tests.mozilla.org", + name: "extension 8", + version: "1.0", + + // Not blocklisted because we are a different ABI + notBlocklisted: [["2", "1.9"]], + }, + { + id: "test_bug393285_9@tests.mozilla.org", + name: "extension 9", + version: "1.0", + + // Blocklisted based on ABI + blocklisted: [["2", "1.9"]], + }, + { + id: "test_bug393285_10@tests.mozilla.org", + name: "extension 10", + version: "1.0", + + // Blocklisted based on ABI + blocklisted: [["2", "1.9"]], + }, + { + id: "test_bug393285_11@tests.mozilla.org", + name: "extension 11", + version: "1.0", + + // Doesn't match both os and abi so not blocked + notBlocklisted: [["2", "1.9"]], + }, + { + id: "test_bug393285_12@tests.mozilla.org", + name: "extension 12", + version: "1.0", + + // Doesn't match both os and abi so not blocked + notBlocklisted: [["2", "1.9"]], + }, + { + id: "test_bug393285_13@tests.mozilla.org", + name: "extension 13", + version: "1.0", + + // Doesn't match both os and abi so not blocked + notBlocklisted: [["2", "1.9"]], + }, + { + id: "test_bug393285_14@tests.mozilla.org", + name: "extension 14", + version: "1.0", + + // Matches both os and abi so blocked + blocklisted: [["2", "1.9"]], + }, +]; + +const ADDON_IDS = ADDONS.map(a => a.id); + +const BLOCKLIST_DATA = [ + { + guid: "test_bug393285_2@tests.mozilla.org", + versionRange: [], + }, + { + guid: "test_bug393285_3a@tests.mozilla.org", + versionRange: [{ maxVersion: "1.0", minVersion: "1.0" }], + }, + { + guid: "test_bug393285_3b@tests.mozilla.org", + versionRange: [{ maxVersion: "1.0", minVersion: "1.0" }], + }, + { + guid: "test_bug393285_4@tests.mozilla.org", + versionRange: [ + { + maxVersion: "1.0", + minVersion: "1.0", + targetApplication: [ + { + guid: "xpcshell@tests.mozilla.org", + maxVersion: "1.0", + minVersion: "1.0", + }, + ], + }, + ], + }, + { + guid: "test_bug393285_5@tests.mozilla.org", + os: "Darwin", + versionRange: [], + }, + { + guid: "test_bug393285_6@tests.mozilla.org", + os: "XPCShell", + versionRange: [], + }, + { + guid: "test_bug393285_7@tests.mozilla.org", + os: "Darwin,XPCShell,WINNT", + versionRange: [], + }, + { + guid: "test_bug393285_8@tests.mozilla.org", + xpcomabi: "x86-msvc", + versionRange: [], + }, + { + guid: "test_bug393285_9@tests.mozilla.org", + xpcomabi: "noarch-spidermonkey", + versionRange: [], + }, + { + guid: "test_bug393285_10@tests.mozilla.org", + xpcomabi: "ppc-gcc3,noarch-spidermonkey,x86-msvc", + versionRange: [], + }, + { + guid: "test_bug393285_11@tests.mozilla.org", + os: "Darwin", + xpcomabi: "ppc-gcc3,x86-msvc", + versionRange: [], + }, + { + guid: "test_bug393285_12@tests.mozilla.org", + os: "Darwin", + xpcomabi: "ppc-gcc3,noarch-spidermonkey,x86-msvc", + versionRange: [], + }, + { + guid: "test_bug393285_13@tests.mozilla.org", + os: "XPCShell", + xpcomabi: "ppc-gcc3,x86-msvc", + versionRange: [], + }, + { + guid: "test_bug393285_14@tests.mozilla.org", + os: "XPCShell,WINNT", + xpcomabi: "ppc-gcc3,x86-msvc,noarch-spidermonkey", + versionRange: [], + }, +]; + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9"); + await promiseStartupManager(); + + for (let addon of ADDONS) { + await promiseInstallWebExtension({ + manifest: { + name: addon.name, + version: addon.version, + browser_specific_settings: { gecko: { id: addon.id } }, + }, + }); + } + + let addons = await getAddons(ADDON_IDS); + for (let id of ADDON_IDS) { + equal( + addons.get(id).blocklistState, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED, + `Add-on ${id} should not initially be blocked` + ); + } +}); + +add_task(async function test_1() { + await AddonTestUtils.loadBlocklistRawData({ extensions: BLOCKLIST_DATA }); + + let addons = await getAddons(ADDON_IDS); + async function isBlocklisted(addon, appVer, toolkitVer) { + let state = await Blocklist.getAddonBlocklistState( + addon, + appVer, + toolkitVer + ); + return state != Services.blocklist.STATE_NOT_BLOCKED; + } + for (let addon of ADDONS) { + let { id } = addon; + for (let blocklisted of addon.blocklisted || []) { + ok( + await isBlocklisted(addons.get(id), ...blocklisted), + `Add-on ${id} should be blocklisted in app/platform version ${blocklisted}` + ); + } + for (let notBlocklisted of addon.notBlocklisted || []) { + ok( + !(await isBlocklisted(addons.get(id), ...notBlocklisted)), + `Add-on ${id} should not be blocklisted in app/platform version ${notBlocklisted}` + ); + } + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_prefs.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_prefs.js new file mode 100644 index 0000000000..42eb1305c4 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_prefs.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests resetting of preferences in blocklist entry when an add-on is blocked. +// See bug 802434. + +// useMLBF=true only supports blocking, not resetting prefs, since extensions +// cannot set arbitrary prefs any more after the removal of legacy addons. +enable_blocklist_v2_instead_of_useMLBF(); + +const BLOCKLIST_DATA = [ + { + guid: "block1@tests.mozilla.org", + versionRange: [ + { + severity: "1", + targetApplication: [ + { + guid: "xpcshell@tests.mozilla.org", + maxVersion: "2.*", + minVersion: "1", + }, + ], + }, + ], + prefs: ["test.blocklist.pref1", "test.blocklist.pref2"], + }, + { + guid: "block2@tests.mozilla.org", + versionRange: [ + { + severity: "3", + targetApplication: [ + { + guid: "xpcshell@tests.mozilla.org", + maxVersion: "2.*", + minVersion: "1", + }, + ], + }, + ], + prefs: ["test.blocklist.pref3", "test.blocklist.pref4"], + }, +]; + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + + await promiseStartupManager(); + + // Add 2 extensions + await promiseInstallWebExtension({ + manifest: { + name: "Blocked add-on-1 with to-be-reset prefs", + version: "1.0", + browser_specific_settings: { gecko: { id: "block1@tests.mozilla.org" } }, + }, + }); + await promiseInstallWebExtension({ + manifest: { + name: "Blocked add-on-2 with to-be-reset prefs", + version: "1.0", + browser_specific_settings: { gecko: { id: "block2@tests.mozilla.org" } }, + }, + }); + + // Pre-set the preferences that we expect to get reset. + Services.prefs.setIntPref("test.blocklist.pref1", 15); + Services.prefs.setIntPref("test.blocklist.pref2", 15); + Services.prefs.setBoolPref("test.blocklist.pref3", true); + Services.prefs.setBoolPref("test.blocklist.pref4", true); + + // Before blocklist is loaded. + let [a1, a2] = await AddonManager.getAddonsByIDs([ + "block1@tests.mozilla.org", + "block2@tests.mozilla.org", + ]); + Assert.equal(a1.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + Assert.equal(a2.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + + Assert.equal(Services.prefs.getIntPref("test.blocklist.pref1"), 15); + Assert.equal(Services.prefs.getIntPref("test.blocklist.pref2"), 15); + Assert.equal(Services.prefs.getBoolPref("test.blocklist.pref3"), true); + Assert.equal(Services.prefs.getBoolPref("test.blocklist.pref4"), true); +}); + +add_task(async function test_blocks() { + await AddonTestUtils.loadBlocklistRawData({ extensions: BLOCKLIST_DATA }); + + // Blocklist changes should have applied and the prefs must be reset. + let [a1, a2] = await AddonManager.getAddonsByIDs([ + "block1@tests.mozilla.org", + "block2@tests.mozilla.org", + ]); + Assert.notEqual(a1, null); + Assert.equal(a1.blocklistState, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + Assert.notEqual(a2, null); + Assert.equal(a2.blocklistState, Ci.nsIBlocklistService.STATE_BLOCKED); + + // All these prefs must be reset to defaults. + Assert.equal(Services.prefs.prefHasUserValue("test.blocklist.pref1"), false); + Assert.equal(Services.prefs.prefHasUserValue("test.blocklist.pref2"), false); + Assert.equal(Services.prefs.prefHasUserValue("test.blocklist.pref3"), false); + Assert.equal(Services.prefs.prefHasUserValue("test.blocklist.pref4"), false); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_regexp_split.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_regexp_split.js new file mode 100644 index 0000000000..f48a6b9d8b --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_regexp_split.js @@ -0,0 +1,225 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// useMLBF=true only supports blocking by version+ID, not by regexp. +enable_blocklist_v2_instead_of_useMLBF(); + +const BLOCKLIST_DATA = [ + { + guid: "/^abcd.*/", + versionRange: [], + expectedType: "RegExp", + }, + { + guid: "test@example.com", + versionRange: [], + expectedType: "string", + }, + { + guid: "/^((a)|(b)|(c))$/", + versionRange: [], + expectedType: "Set", + }, + { + guid: "/^((a@b)|(\\{6d9ddd6e-c6ee-46de-ab56-ce9080372b3\\})|(c@d.com))$/", + versionRange: [], + expectedType: "Set", + }, + // The same as the above, but with escape sequences that disqualify it from + // being treated as a set (and a different guid) + { + guid: "/^((s@t)|(\\{6d9eee6e-c6ee-46de-ab56-ce9080372b3\\})|(c@d\\w.com))$/", + versionRange: [], + expectedType: "RegExp", + }, + // Also the same, but with other magical regex characters. + // (and a different guid) + { + guid: "/^((u@v)|(\\{6d9fff6e*-c6ee-46de-ab56-ce9080372b3\\})|(c@dee?.com))$/", + versionRange: [], + expectedType: "RegExp", + }, +]; + +/** + * Verify that both IDs being OR'd in a regex work, + * and that other regular expressions continue being + * used as regular expressions. + */ +add_task(async function test_check_matching_works() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9"); + await promiseStartupManager(); + await AddonTestUtils.loadBlocklistRawData({ + extensions: BLOCKLIST_DATA, + }); + + const { BlocklistPrivate } = ChromeUtils.importESModule( + "resource://gre/modules/Blocklist.sys.mjs" + ); + let parsedEntries = BlocklistPrivate.ExtensionBlocklistRS._entries; + + // Unfortunately, the parsed entries aren't in the same order as the original. + function strForTypeOf(val) { + if (typeof val == "string") { + return "string"; + } + if (val) { + return val.constructor.name; + } + return "other"; + } + for (let type of ["Set", "RegExp", "string"]) { + let numberParsed = parsedEntries.filter(parsedEntry => { + return type == strForTypeOf(parsedEntry.matches.id); + }).length; + let expected = BLOCKLIST_DATA.filter(entry => { + return type == entry.expectedType; + }).length; + Assert.equal( + numberParsed, + expected, + type + " should have expected number of entries" + ); + } + // Shouldn't block everything. + Assert.ok( + !(await Blocklist.getAddonBlocklistEntry({ id: "nonsense", version: "1" })) + ); + // Should block IDs starting with abcd + Assert.ok( + await Blocklist.getAddonBlocklistEntry({ id: "abcde", version: "1" }) + ); + // Should block the literal string listed + Assert.ok( + await Blocklist.getAddonBlocklistEntry({ + id: "test@example.com", + version: "1", + }) + ); + // Should block the IDs in (a)|(b)|(c) + Assert.ok(await Blocklist.getAddonBlocklistEntry({ id: "a", version: "1" })); + Assert.ok(await Blocklist.getAddonBlocklistEntry({ id: "b", version: "1" })); + Assert.ok(await Blocklist.getAddonBlocklistEntry({ id: "c", version: "1" })); + // Should block all the items processed to a set: + Assert.ok( + await Blocklist.getAddonBlocklistEntry({ id: "a@b", version: "1" }) + ); + Assert.ok( + await Blocklist.getAddonBlocklistEntry({ + id: "{6d9ddd6e-c6ee-46de-ab56-ce9080372b3}", + version: "1", + }) + ); + Assert.ok( + await Blocklist.getAddonBlocklistEntry({ id: "c@d.com", version: "1" }) + ); + // Should block items that remained a regex: + Assert.ok( + await Blocklist.getAddonBlocklistEntry({ id: "s@t", version: "1" }) + ); + Assert.ok( + await Blocklist.getAddonBlocklistEntry({ + id: "{6d9eee6e-c6ee-46de-ab56-ce9080372b3}", + version: "1", + }) + ); + Assert.ok( + await Blocklist.getAddonBlocklistEntry({ id: "c@dx.com", version: "1" }) + ); + + Assert.ok( + await Blocklist.getAddonBlocklistEntry({ id: "u@v", version: "1" }) + ); + Assert.ok( + await Blocklist.getAddonBlocklistEntry({ + id: "{6d9fff6eeeeeeee-c6ee-46de-ab56-ce9080372b3}", + version: "1", + }) + ); + Assert.ok( + await Blocklist.getAddonBlocklistEntry({ id: "c@dee.com", version: "1" }) + ); +}); + +// We should be checking all properties, not just the first one we come across. +add_task(async function check_all_properties() { + await AddonTestUtils.loadBlocklistRawData({ + extensions: [ + { + guid: "literal@string.com", + creator: "Foo", + versionRange: [], + }, + { + guid: "/regex.*@regex\\.com/", + creator: "Foo", + versionRange: [], + }, + { + guid: "/^((set@set\\.com)|(anotherset@set\\.com)|(reallyenoughsetsalready@set\\.com))$/", + creator: "Foo", + versionRange: [], + }, + ], + }); + + let { Blocklist } = ChromeUtils.importESModule( + "resource://gre/modules/Blocklist.sys.mjs" + ); + // Check 'wrong' creator doesn't match. + Assert.ok( + !(await Blocklist.getAddonBlocklistEntry({ + id: "literal@string.com", + version: "1", + creator: { name: "Bar" }, + })) + ); + Assert.ok( + !(await Blocklist.getAddonBlocklistEntry({ + id: "regexaaaaa@regex.com", + version: "1", + creator: { name: "Bar" }, + })) + ); + Assert.ok( + !(await Blocklist.getAddonBlocklistEntry({ + id: "set@set.com", + version: "1", + creator: { name: "Bar" }, + })) + ); + + // Check 'wrong' ID doesn't match. + Assert.ok( + !(await Blocklist.getAddonBlocklistEntry({ + id: "someotherid@foo.com", + version: "1", + creator: { name: "Foo" }, + })) + ); + + // Check items matching all filters do match + Assert.ok( + await Blocklist.getAddonBlocklistEntry({ + id: "literal@string.com", + version: "1", + creator: { name: "Foo" }, + }) + ); + Assert.ok( + await Blocklist.getAddonBlocklistEntry({ + id: "regexaaaaa@regex.com", + version: "1", + creator: { name: "Foo" }, + }) + ); + Assert.ok( + await Blocklist.getAddonBlocklistEntry({ + id: "set@set.com", + version: "1", + creator: { name: "Foo" }, + }) + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_severities.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_severities.js new file mode 100644 index 0000000000..fffbb8a51e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_severities.js @@ -0,0 +1,504 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +// useMLBF=true only supports one type of severity (hard block). The value of +// appDisabled in the extension blocklist is checked in test_blocklist_mlbf.js. +enable_blocklist_v2_instead_of_useMLBF(); + +const URI_EXTENSION_BLOCKLIST_DIALOG = + "chrome://mozapps/content/extensions/blocklist.xhtml"; + +// Workaround for Bug 658720 - URL formatter can leak during xpcshell tests +const PREF_BLOCKLIST_ITEM_URL = "extensions.blocklist.itemURL"; +Services.prefs.setCharPref( + PREF_BLOCKLIST_ITEM_URL, + "http://example.com/blocklist/%blockID%" +); + +async function getAddonBlocklistURL(addon) { + let entry = await Blocklist.getAddonBlocklistEntry(addon); + return entry && entry.url; +} + +var ADDONS = [ + { + // Tests how the blocklist affects a disabled add-on + id: "test_bug455906_1@tests.mozilla.org", + name: "Bug 455906 Addon Test 1", + version: "5", + appVersion: "3", + }, + { + // Tests how the blocklist affects an enabled add-on + id: "test_bug455906_2@tests.mozilla.org", + name: "Bug 455906 Addon Test 2", + version: "5", + appVersion: "3", + }, + { + // Tests how the blocklist affects an enabled add-on, to be disabled by the notification + id: "test_bug455906_3@tests.mozilla.org", + name: "Bug 455906 Addon Test 3", + version: "5", + appVersion: "3", + }, + { + // Tests how the blocklist affects a disabled add-on that was already warned about + id: "test_bug455906_4@tests.mozilla.org", + name: "Bug 455906 Addon Test 4", + version: "5", + appVersion: "3", + }, + { + // Tests how the blocklist affects an enabled add-on that was already warned about + id: "test_bug455906_5@tests.mozilla.org", + name: "Bug 455906 Addon Test 5", + version: "5", + appVersion: "3", + }, + { + // Tests how the blocklist affects an already blocked add-on + id: "test_bug455906_6@tests.mozilla.org", + name: "Bug 455906 Addon Test 6", + version: "5", + appVersion: "3", + }, + { + // Tests how the blocklist affects an incompatible add-on + id: "test_bug455906_7@tests.mozilla.org", + name: "Bug 455906 Addon Test 7", + version: "5", + appVersion: "2", + }, + { + // Spare add-on used to ensure we get a notification when switching lists + id: "dummy_bug455906_1@tests.mozilla.org", + name: "Dummy Addon 1", + version: "5", + appVersion: "3", + }, + { + // Spare add-on used to ensure we get a notification when switching lists + id: "dummy_bug455906_2@tests.mozilla.org", + name: "Dummy Addon 2", + version: "5", + appVersion: "3", + }, +]; + +var gNotificationCheck = null; + +// Don't need the full interface, attempts to call other methods will just +// throw which is just fine +var WindowWatcher = { + openWindow(parent, url, name, features, windowArguments) { + // Should be called to list the newly blocklisted items + equal(url, URI_EXTENSION_BLOCKLIST_DIALOG); + + if (gNotificationCheck) { + gNotificationCheck(windowArguments.wrappedJSObject); + } + + // run the code after the blocklist is closed + Services.obs.notifyObservers(null, "addon-blocklist-closed"); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIWindowWatcher"]), +}; + +MockRegistrar.register( + "@mozilla.org/embedcomp/window-watcher;1", + WindowWatcher +); + +function createAddon(addon) { + return promiseInstallWebExtension({ + manifest: { + name: addon.name, + version: addon.version, + browser_specific_settings: { + gecko: { + id: addon.id, + strict_min_version: addon.appVersion, + strict_max_version: addon.appVersion, + }, + }, + }, + }); +} + +const BLOCKLIST_DATA = { + start: { + // Block 4-6 and a dummy: + extensions: [ + { + guid: "test_bug455906_4@tests.mozilla.org", + versionRange: [{ severity: "-1" }], + }, + { + guid: "test_bug455906_5@tests.mozilla.org", + versionRange: [{ severity: "1" }], + }, + { + guid: "test_bug455906_6@tests.mozilla.org", + versionRange: [{ severity: "2" }], + }, + { + guid: "dummy_bug455906_1@tests.mozilla.org", + versionRange: [], + }, + ], + }, + warn: { + // warn for all test add-ons: + extensions: ADDONS.filter(a => a.id.startsWith("test_")).map(a => ({ + guid: a.id, + versionRange: [{ severity: "-1" }], + })), + }, + block: { + // block all test add-ons: + extensions: ADDONS.filter(a => a.id.startsWith("test_")).map(a => ({ + guid: a.id, + blockID: a.id, + versionRange: [], + })), + }, + empty: { + // Block a dummy so there's a change: + extensions: [ + { + guid: "dummy_bug455906_2@tests.mozilla.org", + versionRange: [], + }, + ], + }, +}; + +async function loadBlocklist(id, callback) { + gNotificationCheck = callback; + + await AddonTestUtils.loadBlocklistRawData(BLOCKLIST_DATA[id]); +} + +function create_blocklistURL(blockID) { + let url = Services.urlFormatter.formatURLPref(PREF_BLOCKLIST_ITEM_URL); + url = url.replace(/%blockID%/g, blockID); + return url; +} + +// Before every main test this is the state the add-ons are meant to be in +async function checkInitialState() { + let addons = await AddonManager.getAddonsByIDs(ADDONS.map(a => a.id)); + + checkAddonState(addons[0], { + userDisabled: true, + softDisabled: false, + appDisabled: false, + }); + checkAddonState(addons[1], { + userDisabled: false, + softDisabled: false, + appDisabled: false, + }); + checkAddonState(addons[2], { + userDisabled: false, + softDisabled: false, + appDisabled: false, + }); + checkAddonState(addons[3], { + userDisabled: true, + softDisabled: true, + appDisabled: false, + }); + checkAddonState(addons[4], { + userDisabled: false, + softDisabled: false, + appDisabled: false, + }); + checkAddonState(addons[5], { + userDisabled: false, + softDisabled: false, + appDisabled: true, + }); + checkAddonState(addons[6], { + userDisabled: false, + softDisabled: false, + appDisabled: true, + }); +} + +function checkAddonState(addon, state) { + return checkAddon(addon.id, addon, state); +} + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "3"); + + await promiseStartupManager(); + + // Load the initial blocklist into the profile to check add-ons start in the + // right state. + await AddonTestUtils.loadBlocklistRawData(BLOCKLIST_DATA.start); + + for (let addon of ADDONS) { + await createAddon(addon); + } +}); + +add_task(async function test_1() { + // Tests the add-ons were installed and the initial blocklist applied as expected + + let addons = await AddonManager.getAddonsByIDs(ADDONS.map(a => a.id)); + for (var i = 0; i < ADDONS.length; i++) { + ok(addons[i], `Addon ${i + 1} should be installed correctly`); + } + + checkAddonState(addons[0], { + userDisabled: false, + softDisabled: false, + appDisabled: false, + }); + checkAddonState(addons[1], { + userDisabled: false, + softDisabled: false, + appDisabled: false, + }); + checkAddonState(addons[2], { + userDisabled: false, + softDisabled: false, + appDisabled: false, + }); + + // Warn add-ons should be soft disabled automatically + checkAddonState(addons[3], { + userDisabled: true, + softDisabled: true, + appDisabled: false, + }); + checkAddonState(addons[4], { + userDisabled: true, + softDisabled: true, + appDisabled: false, + }); + + // Blocked and incompatible should be app disabled only + checkAddonState(addons[5], { + userDisabled: false, + softDisabled: false, + appDisabled: true, + }); + checkAddonState(addons[6], { + userDisabled: false, + softDisabled: false, + appDisabled: true, + }); + + // Put the add-ons into the base state + await addons[0].disable(); + await addons[4].enable(); + + await promiseRestartManager(); + await checkInitialState(); + + await loadBlocklist("warn", args => { + dump("Checking notification pt 2\n"); + // This test is artificial, we don't notify for add-ons anymore, see + // https://bugzilla.mozilla.org/show_bug.cgi?id=1257565#c111 . Cleaning this up + // should happen but this patchset is too huge as it is so I'm deferring it. + equal(args.list.length, 2); + }); + + await promiseRestartManager(); + dump("Checking results pt 2\n"); + + addons = await AddonManager.getAddonsByIDs(ADDONS.map(a => a.id)); + + info("Should have disabled this add-on as requested"); + checkAddonState(addons[2], { + userDisabled: true, + softDisabled: true, + appDisabled: false, + }); + + info("The blocked add-on should have changed to soft disabled"); + checkAddonState(addons[5], { + userDisabled: true, + softDisabled: true, + appDisabled: false, + }); + checkAddonState(addons[6], { + userDisabled: true, + softDisabled: true, + appDisabled: true, + }); + + info("These should have been unchanged"); + checkAddonState(addons[0], { + userDisabled: true, + softDisabled: false, + appDisabled: false, + }); + // XXXgijs this is supposed to be not user disabled or soft disabled, but because we don't show + // the dialog, it's disabled anyway. Comment out this assertion for now... + // checkAddonState(addons[1], {userDisabled: false, softDisabled: false, appDisabled: false}); + checkAddonState(addons[3], { + userDisabled: true, + softDisabled: true, + appDisabled: false, + }); + checkAddonState(addons[4], { + userDisabled: false, + softDisabled: false, + appDisabled: false, + }); + + // Back to starting state + await addons[2].enable(); + await addons[5].enable(); + + await promiseRestartManager(); + await loadBlocklist("start"); +}); + +add_task(async function test_pt3() { + await promiseRestartManager(); + await checkInitialState(); + + await loadBlocklist("block", args => { + dump("Checking notification pt 3\n"); + equal(args.list.length, 3); + }); + + await promiseRestartManager(); + dump("Checking results pt 3\n"); + + let addons = await AddonManager.getAddonsByIDs(ADDONS.map(a => a.id)); + + // All should have gained the blocklist state, user disabled as previously + checkAddonState(addons[0], { + userDisabled: true, + softDisabled: false, + appDisabled: true, + }); + checkAddonState(addons[1], { + userDisabled: false, + softDisabled: false, + appDisabled: true, + }); + checkAddonState(addons[2], { + userDisabled: false, + softDisabled: false, + appDisabled: true, + }); + checkAddonState(addons[4], { + userDisabled: false, + softDisabled: false, + appDisabled: true, + }); + + // Should have gained the blocklist state but no longer be soft disabled + checkAddonState(addons[3], { + userDisabled: false, + softDisabled: false, + appDisabled: true, + }); + + // Check blockIDs are correct + equal( + await getAddonBlocklistURL(addons[0]), + create_blocklistURL(addons[0].id) + ); + equal( + await getAddonBlocklistURL(addons[1]), + create_blocklistURL(addons[1].id) + ); + equal( + await getAddonBlocklistURL(addons[2]), + create_blocklistURL(addons[2].id) + ); + equal( + await getAddonBlocklistURL(addons[3]), + create_blocklistURL(addons[3].id) + ); + equal( + await getAddonBlocklistURL(addons[4]), + create_blocklistURL(addons[4].id) + ); + + // Shouldn't be changed + checkAddonState(addons[5], { + userDisabled: false, + softDisabled: false, + appDisabled: true, + }); + checkAddonState(addons[6], { + userDisabled: false, + softDisabled: false, + appDisabled: true, + }); + + // Back to starting state + await loadBlocklist("start"); +}); + +add_task(async function test_pt4() { + let addon = await AddonManager.getAddonByID(ADDONS[4].id); + await addon.enable(); + + await promiseRestartManager(); + await checkInitialState(); + + await loadBlocklist("empty", args => { + dump("Checking notification pt 4\n"); + // See note in other callback - we no longer notify for non-blocked add-ons. + ok(false, "Should not get a notification as there are no blocked addons."); + }); + + await promiseRestartManager(); + dump("Checking results pt 4\n"); + + let addons = await AddonManager.getAddonsByIDs(ADDONS.map(a => a.id)); + // This should have become unblocked + checkAddonState(addons[5], { + userDisabled: false, + softDisabled: false, + appDisabled: false, + }); + + // Should get re-enabled + checkAddonState(addons[3], { + userDisabled: false, + softDisabled: false, + appDisabled: false, + }); + + // No change for anything else + checkAddonState(addons[0], { + userDisabled: true, + softDisabled: false, + appDisabled: false, + }); + checkAddonState(addons[1], { + userDisabled: false, + softDisabled: false, + appDisabled: false, + }); + checkAddonState(addons[2], { + userDisabled: false, + softDisabled: false, + appDisabled: false, + }); + checkAddonState(addons[4], { + userDisabled: false, + softDisabled: false, + appDisabled: false, + }); + checkAddonState(addons[6], { + userDisabled: false, + softDisabled: false, + appDisabled: true, + }); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_statechange_telemetry.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_statechange_telemetry.js new file mode 100644 index 0000000000..6d0f89ff8c --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_statechange_telemetry.js @@ -0,0 +1,411 @@ +// Verifies that changes to blocklistState are correctly reported to telemetry. + +"use strict"; + +Services.prefs.setBoolPref("extensions.blocklist.useMLBF", true); + +// Set min version to 42 because the updater defaults to min version 42. +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42.0", "42.0"); + +// Use unprivileged signatures because the MLBF-based blocklist does not +// apply to add-ons with a privileged signature. +AddonTestUtils.usePrivilegedSignatures = false; + +const { Downloader } = ChromeUtils.importESModule( + "resource://services-settings/Attachments.sys.mjs" +); + +const { TelemetryController } = ChromeUtils.importESModule( + "resource://gre/modules/TelemetryController.sys.mjs" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const ExtensionBlocklistMLBF = getExtensionBlocklistMLBF(); + +const EXT_ID = "maybeblockme@tests.mozilla.org"; + +// The addon blocked by the bloom filter (referenced by MLBF_RECORD). +const EXT_BLOCKED_ID = "@blocked"; +const EXT_BLOCKED_VERSION = "1"; +const EXT_BLOCKED_SIGN_TIME = 12345; // Before MLBF_RECORD.generation_time. + +// To serve updates. +const server = AddonTestUtils.createHttpServer(); +const SERVER_BASE_URL = `http://127.0.0.1:${server.identity.primaryPort}`; +const SERVER_UPDATE_PATH = "/update.json"; +const SERVER_UPDATE_URL = `${SERVER_BASE_URL}${SERVER_UPDATE_PATH}`; +// update is served via `server` over insecure http. +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); + +async function assertEventDetails(expectedExtras) { + if (!IS_ANDROID_BUILD) { + const expectedEvents = expectedExtras.map(expectedExtra => { + let { object, value, ...extra } = expectedExtra; + return ["blocklist", "addonBlockChange", object, value, extra]; + }); + await TelemetryTestUtils.assertEvents(expectedEvents, { + category: "blocklist", + method: "addonBlockChange", + }); + } else { + info( + `Skip assertions on collected samples for addonBlockChange on android builds` + ); + } + assertGleanEventDetails(expectedExtras); +} +async function assertGleanEventDetails(expectedExtras) { + const snapshot = Glean.blocklist.addonBlockChange.testGetValue(); + if (expectedExtras.length === 0) { + Assert.deepEqual(undefined, snapshot, "Expected zero addonBlockChange"); + return; + } + Assert.equal( + expectedExtras.length, + snapshot?.length, + "Number of addonBlockChange records" + ); + for (let i of expectedExtras.keys()) { + let actual = snapshot[i].extra; + // Glean uses snake_case instead of camelCase. + let { blocklistState, ...expected } = expectedExtras[i]; + expected.blocklist_state = blocklistState; + Assert.deepEqual(expected, actual, `Expected addonBlockChange (${i})`); + } +} + +// Stage an update on the update server. The add-on must have been created +// with update_url set to SERVER_UPDATE_URL. +function setupAddonUpdate(addonId, addonVersion) { + let updateXpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version: addonVersion, + browser_specific_settings: { + gecko: { id: addonId, update_url: SERVER_UPDATE_URL }, + }, + }, + }); + let updateXpiPath = `/update-${addonId}-${addonVersion}.xpi`; + server.registerFile(updateXpiPath, updateXpi); + AddonTestUtils.registerJSON(server, SERVER_UPDATE_PATH, { + addons: { + [addonId]: { + updates: [ + { + version: addonVersion, + update_link: `${SERVER_BASE_URL}${updateXpiPath}`, + }, + ], + }, + }, + }); +} + +async function tryAddonInstall(addonId, addonVersion) { + let xpiFile = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version: addonVersion, + browser_specific_settings: { + gecko: { id: addonId, update_url: SERVER_UPDATE_URL }, + }, + }, + }); + const install = await promiseInstallFile(xpiFile, true); + // Passing true to promiseInstallFile means that the xpi may not be installed + // if blocked by the blocklist. In that case, |install| may be null. + return install?.addon; +} + +add_task(async function setup() { + if (!IS_ANDROID_BUILD) { + // FOG needs a profile directory to put its data in. + do_get_profile(); + // FOG needs to be initialized in order for data to flow. + Services.fog.initializeFOG(); + } + await TelemetryController.testSetup(); + + // Disable the packaged record and attachment to make sure that the test + // will not fall back to the packaged attachments. + Downloader._RESOURCE_BASE_URL = "invalid://bogus"; + + await promiseStartupManager(); +}); + +add_task(async function install_update_not_blocked_is_no_events() { + Services.fog.testResetFOG(); + // Install an add-on that is not blocked. Then update to the next version. + let addon = await tryAddonInstall(EXT_ID, "0.1"); + + // Version "1" not blocked yet, but will be in the next test task. + setupAddonUpdate(EXT_ID, "1"); + let update = await AddonTestUtils.promiseFindAddonUpdates(addon); + await promiseCompleteInstall(update.updateAvailable); + addon = await AddonManager.getAddonByID(EXT_ID); + equal(addon.version, "1", "Add-on was updated"); + + await assertEventDetails([]); +}); + +add_task(async function blocklist_update_events() { + Services.fog.testResetFOG(); + const EXT_HOURS_SINCE_INSTALL = 4321; + const addon = await AddonManager.getAddonByID(EXT_ID); + addon.__AddonInternal__.installDate = + addon.installDate.getTime() - 3600000 * EXT_HOURS_SINCE_INSTALL; + + await AddonTestUtils.loadBlocklistRawData({ + extensionsMLBF: [ + { stash: { blocked: [`${EXT_ID}:1`], unblocked: [] }, stash_time: 123 }, + { stash: { blocked: [`${EXT_ID}:2`], unblocked: [] }, stash_time: 456 }, + ], + }); + + await assertEventDetails([ + { + object: "blocklist_update", + value: EXT_ID, + blocklistState: "2", // Ci.nsIBlocklistService.STATE_BLOCKED + addon_version: "1", + signed_date: "0", + hours_since: `${EXT_HOURS_SINCE_INSTALL}`, + mlbf_last_time: "456", + mlbf_generation: "0", + mlbf_source: "unknown", + }, + ]); +}); + +add_task(async function update_check_blocked_by_stash() { + Services.fog.testResetFOG(); + setupAddonUpdate(EXT_ID, "2"); + let addon = await AddonManager.getAddonByID(EXT_ID); + let update = await AddonTestUtils.promiseFindAddonUpdates(addon); + // Blocks in stashes are immediately enforced by update checks. + // Blocks stored in MLBFs are only enforced after the package is downloaded, + // and that scenario is covered by update_check_blocked_by_stash elsewhere. + equal(update.updateAvailable, false, "Update was blocked by stash"); + + await assertEventDetails([ + { + object: "addon_update_check", + value: EXT_ID, + blocklistState: "2", // Ci.nsIBlocklistService.STATE_BLOCKED + addon_version: "2", + signed_date: "0", + hours_since: "-1", + mlbf_last_time: "456", + mlbf_generation: "0", + mlbf_source: "unknown", + }, + ]); +}); + +// Any attempt to re-install a blocked add-on should trigger a telemetry +// event, even though the blocklistState did not change. +add_task(async function reinstall_blocked_addon() { + Services.fog.testResetFOG(); + let blockedAddon = await AddonManager.getAddonByID(EXT_ID); + equal( + blockedAddon.blocklistState, + Ci.nsIBlocklistService.STATE_BLOCKED, + "Addon was initially blocked" + ); + + let addon = await tryAddonInstall(EXT_ID, "2"); + ok(!addon, "Add-on install should be blocked by a stash"); + + await assertEventDetails([ + { + // Note: installs of existing versions are observed as "addon_install". + // Only updates after update checks are tagged as "addon_update". + object: "addon_install", + value: EXT_ID, + blocklistState: "2", // Ci.nsIBlocklistService.STATE_BLOCKED + addon_version: "2", + signed_date: "0", + hours_since: "-1", + mlbf_last_time: "456", + mlbf_generation: "0", + mlbf_source: "unknown", + }, + ]); +}); + +// For comparison with the next test task (database_modified), verify that a +// regular restart without database modifications does not trigger events. +add_task(async function regular_restart_no_event() { + Services.fog.testResetFOG(); + // Version different/higher than the 42.0 that was passed to createAppInfo at + // the start of this test file to force a database rebuild. + await promiseRestartManager("90.0"); + await assertEventDetails([]); + + await promiseRestartManager(); + await assertEventDetails([]); +}); + +add_task(async function database_modified() { + Services.fog.testResetFOG(); + const EXT_HOURS_SINCE_INSTALL = 3; + await promiseShutdownManager(); + + // Modify the addon database: blocked->not blocked + decrease installDate. + let addonDB = await IOUtils.readJSON(gExtensionsJSON.path); + let rawAddon = addonDB.addons[0]; + equal(rawAddon.id, EXT_ID, "Expected entry in addonDB"); + equal(rawAddon.blocklistState, 2, "Expected STATE_BLOCKED"); + rawAddon.blocklistState = 0; // STATE_NOT_BLOCKED + rawAddon.installDate = Date.now() - 3600000 * EXT_HOURS_SINCE_INSTALL; + await IOUtils.writeJSON(gExtensionsJSON.path, addonDB); + + // Bump version to force database rebuild. + await promiseStartupManager("91.0"); + // Shut down because the database reconcilation blocks shutdown, and we want + // to be certain that the process has finished before checking the events. + await promiseShutdownManager(); + await assertEventDetails([ + { + object: "addon_db_modified", + value: EXT_ID, + blocklistState: "2", // Ci.nsIBlocklistService.STATE_BLOCKED + addon_version: "1", + signed_date: "0", + hours_since: `${EXT_HOURS_SINCE_INSTALL}`, + mlbf_last_time: "456", + mlbf_generation: "0", + mlbf_source: "unknown", + }, + ]); + + Services.fog.testResetFOG(); + await promiseStartupManager(); + await assertEventDetails([]); +}); + +add_task(async function install_replaces_blocked_addon() { + Services.fog.testResetFOG(); + let addon = await tryAddonInstall(EXT_ID, "3"); + ok(addon, "Update supersedes blocked add-on"); + + await assertEventDetails([ + { + object: "addon_install", + value: EXT_ID, + blocklistState: "0", // Ci.nsIBlocklistService.STATE_NOT_BLOCKED + addon_version: "3", + signed_date: "0", + hours_since: "-1", + mlbf_last_time: "456", + mlbf_generation: "0", + mlbf_source: "unknown", + }, + ]); +}); + +add_task(async function install_blocked_by_mlbf() { + Services.fog.testResetFOG(); + await ExtensionBlocklistMLBF._client.db.saveAttachment( + ExtensionBlocklistMLBF.RS_ATTACHMENT_ID, + { record: MLBF_RECORD, blob: await load_mlbf_record_as_blob() } + ); + await AddonTestUtils.loadBlocklistRawData({ + extensionsMLBF: [MLBF_RECORD], + }); + + AddonTestUtils.certSignatureDate = EXT_BLOCKED_SIGN_TIME; + let addon = await tryAddonInstall(EXT_BLOCKED_ID, EXT_BLOCKED_VERSION); + AddonTestUtils.certSignatureDate = null; + + ok(!addon, "Add-on install should be blocked by the MLBF"); + + await assertEventDetails([ + { + object: "addon_install", + value: EXT_BLOCKED_ID, + blocklistState: "2", // Ci.nsIBlocklistService.STATE_BLOCKED + addon_version: EXT_BLOCKED_VERSION, + signed_date: `${EXT_BLOCKED_SIGN_TIME}`, + hours_since: "-1", + // When there is no stash at all, the MLBF's generation_time is used. + mlbf_last_time: `${MLBF_RECORD.generation_time}`, + mlbf_generation: `${MLBF_RECORD.generation_time}`, + mlbf_source: "cache_match", + }, + ]); +}); + +// A limitation of the MLBF-based blocklist is that it needs the add-on package +// in order to check its signature date. +// This part of the test verifies that installation of the add-on is blocked, +// despite the update check tentatively accepting the package. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1649896 for rationale. +add_task(async function update_check_blocked_by_mlbf() { + Services.fog.testResetFOG(); + // Install a version that we can update, lower than EXT_BLOCKED_VERSION. + let addon = await tryAddonInstall(EXT_BLOCKED_ID, "0.1"); + + setupAddonUpdate(EXT_BLOCKED_ID, EXT_BLOCKED_VERSION); + AddonTestUtils.certSignatureDate = EXT_BLOCKED_SIGN_TIME; + let update = await AddonTestUtils.promiseFindAddonUpdates(addon); + ok(update.updateAvailable, "Update was not blocked by stash"); + + await promiseCompleteInstall(update.updateAvailable); + AddonTestUtils.certSignatureDate = null; + + addon = await AddonManager.getAddonByID(EXT_BLOCKED_ID); + equal(addon.version, EXT_BLOCKED_VERSION, "Add-on was updated"); + equal( + addon.blocklistState, + Ci.nsIBlocklistService.STATE_BLOCKED, + "Add-on is blocked" + ); + equal(addon.appDisabled, true, "Add-on was disabled because of the block"); + + await assertEventDetails([ + { + object: "addon_update", + value: EXT_BLOCKED_ID, + blocklistState: "2", // Ci.nsIBlocklistService.STATE_BLOCKED + addon_version: EXT_BLOCKED_VERSION, + signed_date: `${EXT_BLOCKED_SIGN_TIME}`, + hours_since: "-1", + mlbf_last_time: `${MLBF_RECORD.generation_time}`, + mlbf_generation: `${MLBF_RECORD.generation_time}`, + mlbf_source: "cache_match", + }, + ]); +}); + +add_task(async function update_blocked_to_unblocked() { + Services.fog.testResetFOG(); + // was blocked in update_check_blocked_by_mlbf. + let blockedAddon = await AddonManager.getAddonByID(EXT_BLOCKED_ID); + + // 3 is higher than EXT_BLOCKED_VERSION. + setupAddonUpdate(EXT_BLOCKED_ID, "3"); + AddonTestUtils.certSignatureDate = EXT_BLOCKED_SIGN_TIME; + let update = await AddonTestUtils.promiseFindAddonUpdates(blockedAddon); + ok(update.updateAvailable, "Found an update"); + + await promiseCompleteInstall(update.updateAvailable); + AddonTestUtils.certSignatureDate = null; + + let addon = await AddonManager.getAddonByID(EXT_BLOCKED_ID); + equal(addon.appDisabled, false, "Add-on was re-enabled after unblock"); + await assertEventDetails([ + { + object: "addon_update", + value: EXT_BLOCKED_ID, + blocklistState: "0", // Ci.nsIBlocklistService.STATE_NOT_BLOCKED + addon_version: "3", + signed_date: `${EXT_BLOCKED_SIGN_TIME}`, + hours_since: "-1", + mlbf_last_time: `${MLBF_RECORD.generation_time}`, + mlbf_generation: `${MLBF_RECORD.generation_time}`, + mlbf_source: "cache_match", + }, + ]); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_targetapp_filter.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_targetapp_filter.js new file mode 100644 index 0000000000..b48700570e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_targetapp_filter.js @@ -0,0 +1,392 @@ +const { BlocklistPrivate } = ChromeUtils.importESModule( + "resource://gre/modules/Blocklist.sys.mjs" +); +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); + +const APP_ID = "xpcshell@tests.mozilla.org"; +const TOOLKIT_ID = "toolkit@mozilla.org"; + +let client; + +async function clear_state() { + // Clear local DB. + await client.db.clear(); +} + +async function createRecords(records) { + const withId = records.map((record, i) => ({ + id: `record-${i}`, + ...record, + })); + // Prevent packaged dump to be loaded with high collection timestamp + return client.db.importChanges({}, Date.now(), withId); +} + +function run_test() { + AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "58", + "" + ); + // This will initialize the remote settings clients for blocklists, + // with their specific options etc. + BlocklistPrivate.ExtensionBlocklistRS.ensureInitialized(); + // Obtain one of the instantiated client for our tests. + client = RemoteSettings("addons", { bucketName: "blocklists" }); + + run_next_test(); +} + +add_task(async function test_supports_filter_expressions() { + await createRecords([ + { + name: "My Extension", + filter_expression: 'env.appinfo.ID == "xpcshell@tests.mozilla.org"', + }, + { + name: "My Extension", + filter_expression: "1 == 2", + }, + ]); + + const list = await client.get(); + equal(list.length, 1); +}); +add_task(clear_state); + +add_task(async function test_returns_all_without_target() { + await createRecords([ + { + name: "My Extension", + }, + { + name: "foopydoo", + versionRange: [], + }, + { + name: "My Other Extension", + versionRange: [ + { + severity: 0, + targetApplication: [], + }, + ], + }, + { + name: "Java(\\(TM\\))? Plug-in 11\\.(7[6-9]|[8-9]\\d|1([0-6]\\d|70))(\\.\\d+)?([^\\d\\._]|$)", + versionRange: [ + { + severity: 0, + }, + ], + matchFilename: "libnpjp2\\.so", + }, + { + name: "foopydoo", + versionRange: [ + { + targetApplication: [], + maxVersion: "1", + minVersion: "0", + severity: "1", + }, + ], + }, + ]); + + const list = await client.get(); + equal(list.length, 5); +}); +add_task(clear_state); + +add_task(async function test_returns_without_guid_or_with_matching_guid() { + await createRecords([ + { + willMatch: true, + name: "foopydoo", + versionRange: [ + { + targetApplication: [{}], + }, + ], + }, + { + willMatch: false, + name: "foopydoo", + versionRange: [ + { + targetApplication: [ + { + guid: "some-guid", + }, + ], + }, + ], + }, + { + willMatch: true, + name: "foopydoo", + versionRange: [ + { + targetApplication: [ + { + guid: APP_ID, + }, + ], + }, + ], + }, + { + willMatch: true, + name: "foopydoo", + versionRange: [ + { + targetApplication: [ + { + guid: TOOLKIT_ID, + }, + ], + }, + ], + }, + ]); + + const list = await client.get(); + info(JSON.stringify(list, null, 2)); + equal(list.length, 3); + ok(list.every(e => e.willMatch)); +}); +add_task(clear_state); + +add_task( + async function test_returns_without_app_version_or_with_matching_version() { + await createRecords([ + { + willMatch: true, + name: "foopydoo", + versionRange: [ + { + targetApplication: [ + { + guid: APP_ID, + }, + ], + }, + ], + }, + { + willMatch: true, + name: "foopydoo", + versionRange: [ + { + targetApplication: [ + { + guid: APP_ID, + minVersion: "0", + }, + ], + }, + ], + }, + { + willMatch: true, + name: "foopydoo", + versionRange: [ + { + targetApplication: [ + { + guid: APP_ID, + minVersion: "0", + maxVersion: "9999", + }, + ], + }, + ], + }, + { + willMatch: false, + name: "foopydoo", + versionRange: [ + { + targetApplication: [ + { + guid: APP_ID, + minVersion: "0", + maxVersion: "1", + }, + ], + }, + ], + }, + { + willMatch: true, + name: "foopydoo", + versionRange: [ + { + targetApplication: [ + { + guid: TOOLKIT_ID, + minVersion: "0", + }, + ], + }, + ], + }, + { + willMatch: true, + name: "foopydoo", + versionRange: [ + { + targetApplication: [ + { + guid: TOOLKIT_ID, + minVersion: "0", + maxVersion: "9999", + }, + ], + }, + ], + // We can't test the false case with maxVersion for toolkit, because the toolkit version + // is 0 in xpcshell. + }, + ]); + + const list = await client.get(); + equal(list.length, 5); + ok(list.every(e => e.willMatch)); + } +); +add_task(clear_state); + +add_task(async function test_multiple_version_and_target_applications() { + await createRecords([ + { + willMatch: true, + name: "foopydoo", + versionRange: [ + { + targetApplication: [ + { + guid: "other-guid", + }, + ], + }, + { + targetApplication: [ + { + guid: APP_ID, + minVersion: "0", + maxVersion: "*", + }, + ], + }, + ], + }, + { + willMatch: true, + name: "foopydoo", + versionRange: [ + { + targetApplication: [ + { + guid: "other-guid", + }, + ], + }, + { + targetApplication: [ + { + guid: APP_ID, + minVersion: "0", + }, + ], + }, + ], + }, + { + willMatch: false, + name: "foopydoo", + versionRange: [ + { + targetApplication: [ + { + guid: APP_ID, + maxVersion: "57.*", + }, + ], + }, + { + targetApplication: [ + { + guid: APP_ID, + maxVersion: "56.*", + }, + { + guid: APP_ID, + maxVersion: "57.*", + }, + ], + }, + ], + }, + ]); + + const list = await client.get(); + equal(list.length, 2); + ok(list.every(e => e.willMatch)); +}); +add_task(clear_state); + +add_task(async function test_complex_version() { + await createRecords([ + { + willMatch: false, + name: "foopydoo", + versionRange: [ + { + targetApplication: [ + { + guid: APP_ID, + maxVersion: "57.*", + }, + ], + }, + ], + }, + { + willMatch: true, + name: "foopydoo", + versionRange: [ + { + targetApplication: [ + { + guid: APP_ID, + maxVersion: "9999.*", + }, + ], + }, + ], + }, + { + willMatch: true, + name: "foopydoo", + versionRange: [ + { + targetApplication: [ + { + guid: APP_ID, + minVersion: "19.0a1", + }, + ], + }, + ], + }, + ]); + + const list = await client.get(); + equal(list.length, 2); +}); +add_task(clear_state); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_telemetry.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_telemetry.js new file mode 100644 index 0000000000..5d229ca23a --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_telemetry.js @@ -0,0 +1,138 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +AddonTestUtils.init(this); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "49" +); + +const { TelemetryController } = ChromeUtils.importESModule( + "resource://gre/modules/TelemetryController.sys.mjs" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +add_setup({ skip_if: () => IS_ANDROID_BUILD }, function test_setup() { + // FOG needs a profile directory to put its data in. + do_get_profile(); + + // FOG needs to be initialized in order for data to flow. + Services.fog.initializeFOG(); +}); + +function assertTelemetryScalars(expectedScalars) { + if (!IS_ANDROID_BUILD) { + let scalars = TelemetryTestUtils.getProcessScalars("parent"); + + for (const scalarName of Object.keys(expectedScalars || {})) { + equal( + scalars[scalarName], + expectedScalars[scalarName], + `Got the expected value for ${scalarName} scalar` + ); + } + } else { + info( + `Skip assertions on collected samples for ${expectedScalars} on android builds` + ); + } +} + +add_task(async function test_setup() { + // Ensure that the telemetry scalar definitions are loaded and the + // AddonManager initialized. + await TelemetryController.testSetup(); + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function test_blocklist_lastModified_rs_scalars() { + Services.fog.testResetFOG(); + const now = Date.now(); + + const lastEntryTimes = { + addons: now - 5000, + addons_mlbf: now - 4000, + }; + + const lastEntryTimesUTC = {}; + const toUTC = t => new Date(t).toUTCString(); + for (const key of Object.keys(lastEntryTimes)) { + lastEntryTimesUTC[key] = toUTC(lastEntryTimes[key]); + } + + const { + BlocklistPrivate: { + BlocklistTelemetry, + ExtensionBlocklistMLBF, + ExtensionBlocklistRS, + }, + } = ChromeUtils.importESModule("resource://gre/modules/Blocklist.sys.mjs"); + + // Return a promise resolved when the recordRSBlocklistLastModified method + // has been called (by temporarily replacing the method with a function that + // calls the real method and then resolve the promise). + function promiseScalarRecorded() { + return new Promise(resolve => { + let origFn = BlocklistTelemetry.recordRSBlocklistLastModified; + BlocklistTelemetry.recordRSBlocklistLastModified = async (...args) => { + BlocklistTelemetry.recordRSBlocklistLastModified = origFn; + let res = await origFn.apply(BlocklistTelemetry, args); + resolve(); + return res; + }; + }); + } + + async function fakeRemoteSettingsSync(rsClient, lastModified) { + await rsClient.db.importChanges({}, lastModified); + await rsClient.emit("sync"); + } + + assertTelemetryScalars({ + "blocklist.lastModified_rs_addons_mlbf": undefined, + }); + Assert.equal( + undefined, + Glean.blocklist.lastModifiedRsAddonsMblf.testGetValue() + ); + + info("Test RS addon blocklist lastModified scalar"); + + await ExtensionBlocklistRS.ensureInitialized(); + await Promise.all([ + promiseScalarRecorded(), + fakeRemoteSettingsSync(ExtensionBlocklistRS._client, lastEntryTimes.addons), + ]); + + assertTelemetryScalars({ + "blocklist.lastModified_rs_addons_mlbf": undefined, + }); + + Assert.equal( + undefined, + Glean.blocklist.lastModifiedRsAddonsMblf.testGetValue() + ); + + await ExtensionBlocklistMLBF.ensureInitialized(); + await Promise.all([ + promiseScalarRecorded(), + fakeRemoteSettingsSync( + ExtensionBlocklistMLBF._client, + lastEntryTimes.addons_mlbf + ), + ]); + + assertTelemetryScalars({ + "blocklist.lastModified_rs_addons_mlbf": lastEntryTimesUTC.addons_mlbf, + }); + Assert.equal( + new Date(lastEntryTimesUTC.addons_mlbf).getTime(), + Glean.blocklist.lastModifiedRsAddonsMblf.testGetValue().getTime() + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklistchange.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklistchange.js new file mode 100644 index 0000000000..7383e093ee --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklistchange.js @@ -0,0 +1,1389 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Checks that changes that cause an add-on to become unblocked or blocked have +// the right effect + +// The tests follow a mostly common pattern. First they start with the add-ons +// unblocked, then they make a change that causes the add-ons to become blocked +// then they make a similar change that keeps the add-ons blocked then they make +// a change that unblocks the add-ons. Some tests skip the initial part and +// start with add-ons detected as blocked. + +// softblock1 is enabled/disabled by the blocklist changes so its softDisabled +// property should always match its userDisabled property + +// softblock2 gets manually enabled then disabled after it becomes blocked so +// its softDisabled property should never become true after that + +// softblock3 does the same as softblock2 however it remains disabled + +// softblock4 is disabled while unblocked and so should never have softDisabled +// set to true and stay userDisabled. This add-on is not used in tests that +// start with add-ons blocked as it would be identical to softblock3 + +const URI_EXTENSION_BLOCKLIST_DIALOG = + "chrome://mozapps/content/extensions/blocklist.xhtml"; + +// Allow insecure updates +Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false); + +// TODO bug 1649906: strip blocklist v2-specific parts of this test. +// All specific logic is already covered by other test files, but the tests +// here trigger the logic via higher-level methods, so it may make sense to +// keep this file even after the removal of blocklist v2. +const useMLBF = Services.prefs.getBoolPref( + "extensions.blocklist.useMLBF", + true +); + +var testserver = createHttpServer({ hosts: ["example.com"] }); + +function permissionPromptHandler(subject, topic, data) { + ok( + subject?.wrappedJSObject?.info?.resolve, + "Got a permission prompt notification as expected" + ); + subject.wrappedJSObject.info.resolve(); +} + +Services.obs.addObserver( + permissionPromptHandler, + "webextension-permission-prompt" +); + +registerCleanupFunction(() => { + Services.obs.removeObserver( + permissionPromptHandler, + "webextension-permission-prompt" + ); +}); + +const XPIS = {}; + +const ADDON_IDS = [ + "softblock1@tests.mozilla.org", + "softblock2@tests.mozilla.org", + "softblock3@tests.mozilla.org", + "softblock4@tests.mozilla.org", + "hardblock@tests.mozilla.org", + "regexpblock@tests.mozilla.org", +]; + +const BLOCK_APP = [ + { + guid: "xpcshell@tests.mozilla.org", + maxVersion: "2.*", + minVersion: "2", + }, +]; +// JEXL filter expression that matches BLOCK_APP. +const BLOCK_APP_FILTER_EXPRESSION = `env.appinfo.ID == "xpcshell@tests.mozilla.org" && env.appinfo.version >= "2" && env.appinfo.version < "3"`; + +function softBlockApp(id) { + return { + guid: `${id}@tests.mozilla.org`, + versionRange: [ + { + severity: "1", + targetApplication: BLOCK_APP, + }, + ], + }; +} + +function softBlockAddonChange(id) { + return { + guid: `${id}@tests.mozilla.org`, + versionRange: [ + { + severity: "1", + minVersion: "2", + maxVersion: "3", + }, + ], + }; +} + +function softBlockUpdate2(id) { + return { + guid: `${id}@tests.mozilla.org`, + versionRange: [{ severity: "1" }], + }; +} + +function softBlockManual(id) { + return { + guid: `${id}@tests.mozilla.org`, + versionRange: [ + { + maxVersion: "2", + minVersion: "1", + severity: "1", + }, + ], + }; +} + +const BLOCKLIST_DATA = { + empty_blocklist: [], + app_update: [ + softBlockApp("softblock1"), + softBlockApp("softblock2"), + softBlockApp("softblock3"), + softBlockApp("softblock4"), + { + guid: "hardblock@tests.mozilla.org", + versionRange: [ + { + targetApplication: BLOCK_APP, + }, + ], + }, + { + guid: "/^RegExp/", + versionRange: [ + { + severity: "1", + targetApplication: BLOCK_APP, + }, + ], + }, + { + guid: "/^RegExp/i", + versionRange: [ + { + targetApplication: BLOCK_APP, + }, + ], + }, + ], + addon_change: [ + softBlockAddonChange("softblock1"), + softBlockAddonChange("softblock2"), + softBlockAddonChange("softblock3"), + softBlockAddonChange("softblock4"), + { + guid: "hardblock@tests.mozilla.org", + versionRange: [ + { + maxVersion: "3", + minVersion: "2", + }, + ], + }, + { + _comment: + "Two RegExp matches, so test flags work - first shouldn't match.", + guid: "/^RegExp/", + versionRange: [ + { + maxVersion: "3", + minVersion: "2", + severity: "1", + }, + ], + }, + { + guid: "/^RegExp/i", + versionRange: [ + { + maxVersion: "3", + minVersion: "2", + severity: "2", + }, + ], + }, + ], + blocklist_update2: [ + softBlockUpdate2("softblock1"), + softBlockUpdate2("softblock2"), + softBlockUpdate2("softblock3"), + softBlockUpdate2("softblock4"), + { + guid: "hardblock@tests.mozilla.org", + versionRange: [], + }, + { + guid: "/^RegExp/", + versionRange: [{ severity: "1" }], + }, + { + guid: "/^RegExp/i", + versionRange: [], + }, + ], + manual_update: [ + softBlockManual("softblock1"), + softBlockManual("softblock2"), + softBlockManual("softblock3"), + softBlockManual("softblock4"), + { + guid: "hardblock@tests.mozilla.org", + versionRange: [ + { + maxVersion: "2", + minVersion: "1", + }, + ], + }, + { + guid: "/^RegExp/i", + versionRange: [ + { + maxVersion: "2", + minVersion: "1", + }, + ], + }, + ], +}; + +// Blocklist v3 (useMLBF) only supports hard blocks by guid+version. Version +// ranges, regexps and soft blocks are not supported. So adjust expectations to +// ensure that the test passes even if useMLBF=true, by: +// - soft blocks are converted to hard blocks. +// - hard blocks are accepted as-is. +// - regexps blocks are converted to hard blocks. +// - Version ranges are expanded to cover all known versions. +if (useMLBF) { + for (let [key, blocks] of Object.entries(BLOCKLIST_DATA)) { + BLOCKLIST_DATA[key] = []; + for (let block of blocks) { + let { guid } = block; + if (guid.includes("RegExp")) { + guid = "regexpblock@tests.mozilla.org"; + } else if (!guid.startsWith("soft") && !guid.startsWith("hard")) { + throw new Error(`Unexpected mock addon ID: ${guid}`); + } + + const { + minVersion = "1", + maxVersion = "3", + targetApplication, + } = block.versionRange?.[0] || {}; + + for (let v = minVersion; v <= maxVersion; ++v) { + BLOCKLIST_DATA[key].push({ + // Assume that IF targetApplication is set, that it is BLOCK_APP. + filter_expression: targetApplication && BLOCK_APP_FILTER_EXPRESSION, + stash: { + // XPI files use version `${v}.0`, update manifests use `${v}`. + blocked: [`${guid}:${v}.0`, `${guid}:${v}`], + unblocked: [], + }, + }); + } + } + } +} + +// XXXgijs: according to https://bugzilla.mozilla.org/show_bug.cgi?id=1257565#c111 +// this code and the related code in Blocklist.jsm (specific to XML blocklist) is +// dead code and can be removed. See https://bugzilla.mozilla.org/show_bug.cgi?id=1549550 . +// +// Don't need the full interface, attempts to call other methods will just +// throw which is just fine +var WindowWatcher = { + openWindow(parent, url, name, features, openArgs) { + // Should be called to list the newly blocklisted items + Assert.equal(url, URI_EXTENSION_BLOCKLIST_DIALOG); + + // Simulate auto-disabling any softblocks + var list = openArgs.wrappedJSObject.list; + list.forEach(function (aItem) { + if (!aItem.blocked) { + aItem.disable = true; + } + }); + + // run the code after the blocklist is closed + Services.obs.notifyObservers(null, "addon-blocklist-closed"); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIWindowWatcher"]), +}; + +MockRegistrar.register( + "@mozilla.org/embedcomp/window-watcher;1", + WindowWatcher +); + +var InstallConfirm = { + confirm(aWindow, aUrl, aInstalls) { + aInstalls.forEach(function (aInstall) { + aInstall.install(); + }); + }, + + QueryInterface: ChromeUtils.generateQI(["amIWebInstallPrompt"]), +}; + +var InstallConfirmFactory = { + createInstance: function createInstance(iid) { + return InstallConfirm.QueryInterface(iid); + }, +}; + +var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); +registrar.registerFactory( + Components.ID("{f0863905-4dde-42e2-991c-2dc8209bc9ca}"), + "Fake Install Prompt", + "@mozilla.org/addons/web-install-prompt;1", + InstallConfirmFactory +); + +function Pload_blocklist(aId) { + return AddonTestUtils.loadBlocklistRawData({ + [useMLBF ? "extensionsMLBF" : "extensions"]: BLOCKLIST_DATA[aId], + }); +} + +// Does a background update check for add-ons and returns a promise that +// resolves when any started installs complete +function Pbackground_update() { + return new Promise((resolve, reject) => { + let installCount = 0; + let backgroundCheckCompleted = false; + + AddonManager.addInstallListener({ + onNewInstall(aInstall) { + installCount++; + }, + + onInstallEnded(aInstall) { + installCount--; + // Wait until all started installs have completed + if (installCount) { + return; + } + + AddonManager.removeInstallListener(this); + + // If the background check hasn't yet completed then let that call the + // callback when it is done + if (!backgroundCheckCompleted) { + return; + } + + resolve(); + }, + }); + + Services.obs.addObserver(function observer() { + Services.obs.removeObserver( + observer, + "addons-background-update-complete" + ); + backgroundCheckCompleted = true; + + // If any new installs have started then we'll call the callback once they + // are completed + if (installCount) { + return; + } + + resolve(); + }, "addons-background-update-complete"); + + AddonManagerPrivate.backgroundUpdateCheck(); + }); +} + +// Manually updates the test add-ons to the given version +function Pmanual_update(aVersion) { + const names = ["soft1", "soft2", "soft3", "soft4", "hard", "regexp"]; + return Promise.all( + names.map(async name => { + let url = `http://example.com/addons/blocklist_${name}_${aVersion}.xpi`; + let install = await AddonManager.getInstallForURL(url); + + // installAddonFromAOM() does more checking than install.install(). + // In particular, it will refuse to install an incompatible addon. + + return new Promise(resolve => { + install.addListener({ + onDownloadCancelled: resolve, + onInstallEnded: resolve, + }); + + AddonManager.installAddonFromAOM(null, null, install); + }); + }) + ); +} + +// Checks that an add-ons properties match expected values +function check_addon( + aAddon, + aExpectedVersion, + aExpectedUserDisabled, + aExpectedSoftDisabled, + aExpectedState +) { + if (useMLBF) { + if (aAddon.id.startsWith("soft")) { + if (aExpectedState === Ci.nsIBlocklistService.STATE_SOFTBLOCKED) { + // The whole test file assumes that an add-on is "user-disabled" after + // an explicit disable(), or after a soft block (without enable()). + // With useMLBF, soft blocks are not supported, so the "user-disabled" + // state matches the usual behavior of "userDisabled" (=disable()). + aExpectedUserDisabled = aAddon.userDisabled; + aExpectedSoftDisabled = false; + aExpectedState = Ci.nsIBlocklistService.STATE_BLOCKED; + } + } + } + + Assert.notEqual(aAddon, null); + info( + "Testing " + + aAddon.id + + " version " + + aAddon.version + + " user " + + aAddon.userDisabled + + " soft " + + aAddon.softDisabled + + " perms " + + aAddon.permissions + ); + + Assert.equal(aAddon.version, aExpectedVersion); + Assert.equal(aAddon.blocklistState, aExpectedState); + Assert.equal(aAddon.userDisabled, aExpectedUserDisabled); + Assert.equal(aAddon.softDisabled, aExpectedSoftDisabled); + if (aAddon.softDisabled) { + Assert.ok(aAddon.userDisabled); + } + + if (aExpectedState == Ci.nsIBlocklistService.STATE_BLOCKED) { + info("blocked, PERM_CAN_ENABLE " + aAddon.id); + Assert.ok(!hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE)); + info("blocked, PERM_CAN_DISABLE " + aAddon.id); + Assert.ok(!hasFlag(aAddon.permissions, AddonManager.PERM_CAN_DISABLE)); + } else if (aAddon.userDisabled) { + info("userDisabled, PERM_CAN_ENABLE " + aAddon.id); + Assert.ok(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE)); + info("userDisabled, PERM_CAN_DISABLE " + aAddon.id); + Assert.ok(!hasFlag(aAddon.permissions, AddonManager.PERM_CAN_DISABLE)); + } else { + info("other, PERM_CAN_ENABLE " + aAddon.id); + Assert.ok(!hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE)); + if (aAddon.type != "theme") { + info("other, PERM_CAN_DISABLE " + aAddon.id); + Assert.ok(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_DISABLE)); + } + } + Assert.equal( + aAddon.appDisabled, + aExpectedState == Ci.nsIBlocklistService.STATE_BLOCKED + ); + + let willBeActive = aAddon.isActive; + if (hasFlag(aAddon.pendingOperations, AddonManager.PENDING_DISABLE)) { + willBeActive = false; + } else if (hasFlag(aAddon.pendingOperations, AddonManager.PENDING_ENABLE)) { + willBeActive = true; + } + + if ( + aExpectedUserDisabled || + aExpectedState == Ci.nsIBlocklistService.STATE_BLOCKED + ) { + Assert.ok(!willBeActive); + } else { + Assert.ok(willBeActive); + } +} + +async function promiseRestartManagerWithAppChange(version) { + await promiseShutdownManager(); + await promiseStartupManagerWithAppChange(version); +} + +async function promiseStartupManagerWithAppChange(version) { + if (version) { + AddonTestUtils.appInfo.version = version; + } + if (useMLBF) { + // The old ExtensionBlocklist enforced the app version/ID part of the block + // when the blocklist entry is checked. + // The new ExtensionBlocklist (with useMLBF=true) does not separately check + // the app version/ID, but the underlying data source (Remote Settings) + // does offer the ability to filter entries with `filter_expression`. + // Force a reload to ensure that the BLOCK_APP_FILTER_EXPRESSION filter in + // this test file is checked again against the new version. + await Blocklist.ExtensionBlocklist._updateMLBF(); + } + await promiseStartupManager(); +} + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + if (useMLBF) { + const { ClientEnvironmentBase } = ChromeUtils.importESModule( + "resource://gre/modules/components-utils/ClientEnvironment.sys.mjs" + ); + Object.defineProperty(ClientEnvironmentBase, "appinfo", { + configurable: true, + get() { + return gAppInfo; + }, + }); + } + + function getxpibasename(id, version) { + // pattern used to map ids like softblock1 to soft1 + let pattern = /^(soft|hard|regexp)block([1-9]*)@/; + let match = id.match(pattern); + return `blocklist_${match[1]}${match[2]}_${version}`; + } + for (let id of ADDON_IDS) { + for (let version of [1, 2, 3, 4]) { + let name = getxpibasename(id, version); + + let xpi = createTempWebExtensionFile({ + manifest: { + name: "Test", + version: `${version}.0`, + browser_specific_settings: { + gecko: { + id, + // This file is generated below, as updateJson. + update_url: `http://example.com/addon_update${version}.json`, + }, + }, + }, + }); + + // To test updates, individual tasks in this test file start the test by + // installing a set of add-ons with version |version| and trigger an + // update check, from XPIS.${nameprefix}${version} (version = 1, 2, 3) + if (version != 4) { + XPIS[name] = xpi; + } + + // update_url above points to a test manifest that references the next + // version. The xpi is made available on the server, so that the test + // can verify that the blocklist works as intended (i.e. update to newer + // version is blocked). + // There is nothing that updates to version 1, only to versions 2, 3, 4. + if (version != 1) { + testserver.registerFile(`/addons/${name}.xpi`, xpi); + } + } + } + + // For each version that this test file uses, create a test manifest that + // references the next version for each id in ADDON_IDS. + for (let version of [1, 2, 3]) { + let updateJson = { addons: {} }; + for (let id of ADDON_IDS) { + let nextversion = version + 1; + let name = getxpibasename(id, nextversion); + updateJson.addons[id] = { + updates: [ + { + applications: { + gecko: { + strict_min_version: "0", + advisory_max_version: "*", + }, + }, + version: `${nextversion}.0`, + update_link: `http://example.com/addons/${name}.xpi`, + }, + ], + }; + } + AddonTestUtils.registerJSON( + testserver, + `/addon_update${version}.json`, + updateJson + ); + } + + await promiseStartupManager(); + + await promiseInstallFile(XPIS.blocklist_soft1_1); + await promiseInstallFile(XPIS.blocklist_soft2_1); + await promiseInstallFile(XPIS.blocklist_soft3_1); + await promiseInstallFile(XPIS.blocklist_soft4_1); + await promiseInstallFile(XPIS.blocklist_hard_1); + await promiseInstallFile(XPIS.blocklist_regexp_1); + + let s4 = await promiseAddonByID("softblock4@tests.mozilla.org"); + await s4.disable(); +}); + +// Starts with add-ons unblocked and then switches application versions to +// change add-ons to blocked and back +add_task(async function run_app_update_test() { + await Pload_blocklist("app_update"); + await promiseRestartManager(); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon( + s1, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon( + s2, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); +}); + +add_task(async function app_update_step_2() { + await promiseRestartManagerWithAppChange("2"); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s2, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s3, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + + await s2.enable(); + await s2.disable(); + check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + await s3.enable(); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_SOFTBLOCKED + ); +}); + +add_task(async function app_update_step_3() { + await promiseRestartManager(); + + await promiseRestartManagerWithAppChange("2.5"); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_SOFTBLOCKED + ); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); +}); + +add_task(async function app_update_step_4() { + await promiseRestartManagerWithAppChange("1"); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon( + s1, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + + await s1.enable(); + await s2.enable(); +}); + +// Starts with add-ons unblocked and then switches application versions to +// change add-ons to blocked and back. A DB schema change is faked to force a +// rebuild when the application version changes +add_task(async function run_app_update_schema_test() { + await promiseRestartManager(); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon( + s1, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon( + s2, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); +}); + +add_task(async function update_schema_2() { + await promiseShutdownManager(); + + await changeXPIDBVersion(100); + gAppInfo.version = "2"; + await promiseStartupManagerWithAppChange(); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s2, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s3, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + + await s2.enable(); + await s2.disable(); + check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + await s3.enable(); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_SOFTBLOCKED + ); +}); + +add_task(async function update_schema_3() { + await promiseRestartManager(); + + await promiseShutdownManager(); + await changeXPIDBVersion(100); + gAppInfo.version = "2.5"; + await promiseStartupManagerWithAppChange(); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_SOFTBLOCKED + ); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); +}); + +add_task(async function update_schema_4() { + await promiseShutdownManager(); + + await changeXPIDBVersion(100); + await promiseStartupManager(); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_SOFTBLOCKED + ); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); +}); + +add_task(async function update_schema_5() { + await promiseShutdownManager(); + + await changeXPIDBVersion(100); + gAppInfo.version = "1"; + await promiseStartupManagerWithAppChange(); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon( + s1, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + + await s1.enable(); + await s2.enable(); +}); + +// Starts with add-ons unblocked and then loads new blocklists to change add-ons +// to blocked and back again. +add_task(async function run_blocklist_update_test() { + await Pload_blocklist("empty_blocklist"); + await promiseRestartManager(); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon( + s1, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon( + s2, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + + await Pload_blocklist("blocklist_update2"); + await promiseRestartManager(); + + [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s2, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s3, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + + await s2.enable(); + await s2.disable(); + check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + await s3.enable(); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_SOFTBLOCKED + ); + + await promiseRestartManager(); + + await Pload_blocklist("blocklist_update2"); + await promiseRestartManager(); + + [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_SOFTBLOCKED + ); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + + await Pload_blocklist("empty_blocklist"); + await promiseRestartManager(); + + [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon( + s1, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + + await s1.enable(); + await s2.enable(); +}); + +// Starts with add-ons unblocked and then new versions are installed outside of +// the app to change them to blocked and back again. +add_task(async function run_addon_change_test() { + await Pload_blocklist("addon_change"); + await promiseRestartManager(); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon( + s1, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon( + s2, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); +}); + +add_task(async function run_addon_change_2() { + await promiseInstallFile(XPIS.blocklist_soft1_2); + await promiseInstallFile(XPIS.blocklist_soft2_2); + await promiseInstallFile(XPIS.blocklist_soft3_2); + await promiseInstallFile(XPIS.blocklist_soft4_2); + await promiseInstallFile(XPIS.blocklist_hard_2); + await promiseInstallFile(XPIS.blocklist_regexp_2); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon(s1, "2.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s2, "2.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s3, "2.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s4, "2.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(h, "2.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + check_addon(r, "2.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + + await s2.enable(); + await s2.disable(); + check_addon(s2, "2.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + await s3.enable(); + check_addon( + s3, + "2.0", + false, + false, + Ci.nsIBlocklistService.STATE_SOFTBLOCKED + ); +}); + +add_task(async function run_addon_change_3() { + await promiseInstallFile(XPIS.blocklist_soft1_3); + await promiseInstallFile(XPIS.blocklist_soft2_3); + await promiseInstallFile(XPIS.blocklist_soft3_3); + await promiseInstallFile(XPIS.blocklist_soft4_3); + await promiseInstallFile(XPIS.blocklist_hard_3); + await promiseInstallFile(XPIS.blocklist_regexp_3); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon(s1, "3.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s2, "3.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon( + s3, + "3.0", + false, + false, + Ci.nsIBlocklistService.STATE_SOFTBLOCKED + ); + check_addon(s4, "3.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(h, "3.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + check_addon(r, "3.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); +}); + +add_task(async function run_addon_change_4() { + await promiseInstallFile(XPIS.blocklist_soft1_1); + await promiseInstallFile(XPIS.blocklist_soft2_1); + await promiseInstallFile(XPIS.blocklist_soft3_1); + await promiseInstallFile(XPIS.blocklist_soft4_1); + await promiseInstallFile(XPIS.blocklist_hard_1); + await promiseInstallFile(XPIS.blocklist_regexp_1); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon( + s1, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + + await s1.enable(); + await s2.enable(); +}); + +// Add-ons are initially unblocked then attempts to upgrade to blocked versions +// in the background which should fail +add_task(async function run_background_update_test() { + await promiseRestartManager(); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon( + s1, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon( + s2, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + + await Pbackground_update(); + await promiseRestartManager(); + + [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon( + s1, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon( + s2, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); +}); + +// Starts with add-ons blocked and then new versions are detected and installed +// automatically for unblocked versions. +add_task(async function run_background_update_2_test() { + await promiseInstallFile(XPIS.blocklist_soft1_3); + await promiseInstallFile(XPIS.blocklist_soft2_3); + await promiseInstallFile(XPIS.blocklist_soft3_3); + await promiseInstallFile(XPIS.blocklist_soft4_3); + await promiseInstallFile(XPIS.blocklist_hard_3); + await promiseInstallFile(XPIS.blocklist_regexp_3); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon(s1, "3.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s2, "3.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s3, "3.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(h, "3.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + check_addon(r, "3.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + + await s2.enable(); + await s2.disable(); + check_addon(s2, "3.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + await s3.enable(); + check_addon( + s3, + "3.0", + false, + false, + Ci.nsIBlocklistService.STATE_SOFTBLOCKED + ); + + await Pbackground_update(); + + [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon( + s1, + "4.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s2, "4.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon( + s3, + "4.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(h, "4.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(r, "4.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + + await s1.enable(); + await s2.enable(); + await s4.disable(); +}); + +// The next test task (run_manual_update_test) was written to expect version 1, +// but after the previous test, version 4 of the add-ons were installed. +add_task(async function reset_addons_to_version_1_instead_of_4() { + await promiseInstallFile(XPIS.blocklist_soft1_1); + await promiseInstallFile(XPIS.blocklist_soft2_1); + await promiseInstallFile(XPIS.blocklist_soft3_1); + await promiseInstallFile(XPIS.blocklist_soft4_1); + await promiseInstallFile(XPIS.blocklist_hard_1); + await promiseInstallFile(XPIS.blocklist_regexp_1); +}); + +// Starts with add-ons blocked and then simulates the user upgrading them to +// unblocked versions. +add_task(async function run_manual_update_test() { + await Pload_blocklist("manual_update"); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s2, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s3, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + + await s2.enable(); + await s2.disable(); + check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + await s3.enable(); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_SOFTBLOCKED + ); + + await Pmanual_update("2"); + + [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + // With useMLBF, s1/s2/s3 are hard blocks, so they cannot update. + const sv2 = useMLBF ? "1.0" : "2.0"; + check_addon(s1, sv2, true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s2, sv2, true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s3, sv2, false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s4, sv2, true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + // Can't manually update to a hardblocked add-on + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + + await Pmanual_update("3"); + + [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon( + s1, + "3.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s2, "3.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon( + s3, + "3.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s4, "3.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(h, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(r, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); +}); + +// Starts with add-ons blocked and then new versions are installed outside of +// the app to change them to unblocked. +add_task(async function run_manual_update_2_test() { + let addons = await promiseAddonsByIDs(ADDON_IDS); + await Promise.all(addons.map(addon => addon.uninstall())); + + await promiseInstallFile(XPIS.blocklist_soft1_1); + await promiseInstallFile(XPIS.blocklist_soft2_1); + await promiseInstallFile(XPIS.blocklist_soft3_1); + await promiseInstallFile(XPIS.blocklist_soft4_1); + await promiseInstallFile(XPIS.blocklist_hard_1); + await promiseInstallFile(XPIS.blocklist_regexp_1); + + let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s2, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s3, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + + await s2.enable(); + await s2.disable(); + check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + await s3.enable(); + check_addon( + s3, + "1.0", + false, + false, + Ci.nsIBlocklistService.STATE_SOFTBLOCKED + ); + + await Pmanual_update("2"); + + [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + // With useMLBF, s1/s2/s3 are hard blocks, so they cannot update. + const sv2 = useMLBF ? "1.0" : "2.0"; + check_addon(s1, sv2, true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s2, sv2, true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s3, sv2, false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + // Can't manually update to a hardblocked add-on + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + + await Pmanual_update("3"); + + [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon( + s1, + "3.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(s2, "3.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon( + s3, + "3.0", + false, + false, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + check_addon(h, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + check_addon(r, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); + + await s1.enable(); + await s2.enable(); + await s4.disable(); +}); + +// Uses the API to install blocked add-ons from the local filesystem +add_task(async function run_local_install_test() { + let addons = await promiseAddonsByIDs(ADDON_IDS); + await Promise.all(addons.map(addon => addon.uninstall())); + + await promiseInstallAllFiles([ + XPIS.blocklist_soft1_1, + XPIS.blocklist_soft2_1, + XPIS.blocklist_soft3_1, + XPIS.blocklist_soft4_1, + XPIS.blocklist_hard_1, + XPIS.blocklist_regexp_1, + ]); + + let installs = await AddonManager.getAllInstalls(); + // Should have finished all installs without needing to restart + Assert.equal(installs.length, 0); + + let [s1, s2, s3 /* s4 */, , h, r] = await promiseAddonsByIDs(ADDON_IDS); + + check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s2, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(s3, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED); + check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); + check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklistchange_v2.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklistchange_v2.js new file mode 100644 index 0000000000..d884438def --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklistchange_v2.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// useMLBF=true doesn't support soft blocks, regexps or version ranges. +// Flip the useMLBF preference to make sure that the test_blocklistchange.js +// test works with and without this pref (blocklist v2 and blocklist v3). +enable_blocklist_v2_instead_of_useMLBF(); + +Services.scriptloader.loadSubScript( + Services.io.newFileURI(do_get_file("test_blocklistchange.js")).spec, + this +); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Device.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Device.js new file mode 100644 index 0000000000..9b1d84b77d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Device.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test whether a machine which differs only on device ID, but otherwise +// exactly matches the blacklist entry, is not blocked. +// Uses test_gfxBlacklist.json + +// Performs the initial setup +async function run_test() { + var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + // We can't do anything if we can't spoof the stuff we need. + if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) { + do_test_finished(); + return; + } + + gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug); + + // Set the vendor/device ID, etc, to match the test file. + switch (Services.appinfo.OS) { + case "WINNT": + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x9876"); + gfxInfo.spoofDriverVersion("8.52.322.2201"); + // Windows 7 + gfxInfo.spoofOSVersion(0x60001); + break; + case "Linux": + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x9876"); + break; + case "Darwin": + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x9876"); + gfxInfo.spoofOSVersion(0xa0900); + break; + case "Android": + gfxInfo.spoofVendorID("abcd"); + gfxInfo.spoofDeviceID("aabb"); + gfxInfo.spoofDriverVersion("5"); + break; + } + + do_test_pending(); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8"); + await promiseStartupManager(); + + function checkBlacklist() { + var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_CANVAS2D_ACCELERATION + ); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + do_test_finished(); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + // If we wait until after we go through the event loop, gfxInfo is sure to + // have processed the gfxItems event. + executeSoon(checkBlacklist); + }, "blocklist-data-gfxItems"); + + mockGfxBlocklistItemsFromDisk("../data/test_gfxBlacklist.json"); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_DriverNew.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_DriverNew.js new file mode 100644 index 0000000000..a1bcde5566 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_DriverNew.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test whether a new-enough driver bypasses the blacklist, even if the rest of +// the attributes match the blacklist entry. +// Uses test_gfxBlacklist.json + +// Performs the initial setup +async function run_test() { + var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + // We can't do anything if we can't spoof the stuff we need. + if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) { + do_test_finished(); + return; + } + + gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug); + + // Set the vendor/device ID, etc, to match the test file. + switch (Services.appinfo.OS) { + case "WINNT": + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x1234"); + gfxInfo.spoofDriverVersion("8.52.322.2202"); + // Windows 7 + gfxInfo.spoofOSVersion(0x60001); + break; + case "Linux": + // We don't support driver versions on Linux. + do_test_finished(); + return; + case "Darwin": + // We don't support driver versions on Darwin. + do_test_finished(); + return; + case "Android": + gfxInfo.spoofVendorID("abcd"); + gfxInfo.spoofDeviceID("wxyz"); + gfxInfo.spoofDriverVersion("6"); + break; + } + + do_test_pending(); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8"); + await promiseStartupManager(); + + function checkBlacklist() { + var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + do_test_finished(); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + // If we wait until after we go through the event loop, gfxInfo is sure to + // have processed the gfxItems event. + executeSoon(checkBlacklist); + }, "blocklist-data-gfxItems"); + + mockGfxBlocklistItemsFromDisk("../data/test_gfxBlacklist.json"); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverNew.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverNew.js new file mode 100644 index 0000000000..ec74d813ae --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverNew.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test whether a machine which is newer than the equal +// blacklist entry is allowed. +// Uses test_gfxBlacklist.json + +// Performs the initial setup +async function run_test() { + var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + // We can't do anything if we can't spoof the stuff we need. + if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) { + do_test_finished(); + return; + } + + gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug); + + // Set the vendor/device ID, etc, to match the test file. + switch (Services.appinfo.OS) { + case "WINNT": + gfxInfo.spoofVendorID("0xdcdc"); + gfxInfo.spoofDeviceID("0x1234"); + // test_gfxBlacklist.json has several entries targeting "os": "All" + // ("All" meaning "All Windows"), with several combinations of + // "driverVersion" / "driverVersionMax" / "driverVersionComparator". + gfxInfo.spoofDriverVersion("8.52.322.1112"); + // Windows 7 + gfxInfo.spoofOSVersion(0x60001); + break; + case "Linux": + // We don't support driver versions on Linux. + // XXX don't we? Seems like we do since bug 1294232 with the change in + // https://hg.mozilla.org/mozilla-central/diff/8962b8d9b7a6/widget/GfxInfoBase.cpp + // To update this test, we'd have to update test_gfxBlacklist.json in a + // way similar to how bug 1714673 was resolved for Android. + do_test_finished(); + return; + case "Darwin": + // We don't support driver versions on OS X. + do_test_finished(); + return; + case "Android": + gfxInfo.spoofVendorID("dcdc"); + gfxInfo.spoofDeviceID("uiop"); + gfxInfo.spoofDriverVersion("6"); + break; + } + + do_test_pending(); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "15.0", "8"); + await promiseStartupManager(); + + function checkBlacklist() { + var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + // Make sure unrelated features aren't affected + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_11_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_OPENGL_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_11_ANGLE); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_HARDWARE_VIDEO_DECODING + ); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_H264 + ); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_DECODE + ); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_ENCODE + ); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBGL_ANGLE); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_CANVAS2D_ACCELERATION + ); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + + do_test_finished(); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + // If we wait until after we go through the event loop, gfxInfo is sure to + // have processed the gfxItems event. + executeSoon(checkBlacklist); + }, "blocklist-data-gfxItems"); + + mockGfxBlocklistItemsFromDisk("../data/test_gfxBlacklist.json"); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverOld.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverOld.js new file mode 100644 index 0000000000..ff887a92eb --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverOld.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test whether a machine which is older than the equal +// blacklist entry is correctly allowed. +// Uses test_gfxBlacklist.json + +// Performs the initial setup +async function run_test() { + var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + // We can't do anything if we can't spoof the stuff we need. + if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) { + do_test_finished(); + return; + } + + gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug); + + // Set the vendor/device ID, etc, to match the test file. + switch (Services.appinfo.OS) { + case "WINNT": + gfxInfo.spoofVendorID("0xdcdc"); + gfxInfo.spoofDeviceID("0x1234"); + gfxInfo.spoofDriverVersion("8.52.322.1110"); + // Windows 7 + gfxInfo.spoofOSVersion(0x60001); + break; + case "Linux": + // We don't support driver versions on Linux. + do_test_finished(); + return; + case "Darwin": + // We don't support driver versions on Darwin. + do_test_finished(); + return; + case "Android": + gfxInfo.spoofVendorID("dcdc"); + gfxInfo.spoofDeviceID("uiop"); + gfxInfo.spoofDriverVersion("4"); + break; + } + + do_test_pending(); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8"); + await promiseStartupManager(); + + function checkBlacklist() { + var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + // Make sure unrelated features aren't affected + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + do_test_finished(); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + // If we wait until after we go through the event loop, gfxInfo is sure to + // have processed the gfxItems event. + executeSoon(checkBlacklist); + }, "blocklist-data-gfxItems"); + + mockGfxBlocklistItemsFromDisk("../data/test_gfxBlacklist.json"); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_OK.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_OK.js new file mode 100644 index 0000000000..1eef119663 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_OK.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test whether a machine which exactly matches the equal +// blacklist entry is successfully blocked. +// Uses test_gfxBlacklist.json + +// Performs the initial setup +async function run_test() { + var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + // We can't do anything if we can't spoof the stuff we need. + if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) { + do_test_finished(); + return; + } + + gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug); + + // Set the vendor/device ID, etc, to match the test file. + switch (Services.appinfo.OS) { + case "WINNT": + gfxInfo.spoofVendorID("0xdcdc"); + gfxInfo.spoofDeviceID("0x1234"); + gfxInfo.spoofDriverVersion("8.52.322.1111"); + // Windows 7 + gfxInfo.spoofOSVersion(0x60001); + break; + case "Linux": + // We don't support driver versions on Linux. + do_test_finished(); + return; + case "Darwin": + // We don't support driver versions on Darwin. + do_test_finished(); + return; + case "Android": + gfxInfo.spoofVendorID("dcdc"); + gfxInfo.spoofDeviceID("uiop"); + gfxInfo.spoofDriverVersion("5"); + break; + } + + do_test_pending(); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8"); + await promiseStartupManager(); + + function checkBlacklist() { + var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + + // Make sure unrelated features aren't affected + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + do_test_finished(); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + // If we wait until after we go through the event loop, gfxInfo is sure to + // have processed the gfxItems event. + executeSoon(checkBlacklist); + }, "blocklist-data-gfxItems"); + + mockGfxBlocklistItemsFromDisk("../data/test_gfxBlacklist.json"); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_DriverOld.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_DriverOld.js new file mode 100644 index 0000000000..182c825ffb --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_DriverOld.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test whether a machine which is lower than the greater-than-or-equal +// blacklist entry is allowed. +// Uses test_gfxBlacklist.json + +// Performs the initial setup +async function run_test() { + var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + // We can't do anything if we can't spoof the stuff we need. + if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) { + do_test_finished(); + return; + } + + gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug); + + // Set the vendor/device ID, etc, to match the test file. + switch (Services.appinfo.OS) { + case "WINNT": + gfxInfo.spoofVendorID("0xabab"); + gfxInfo.spoofDeviceID("0x1234"); + gfxInfo.spoofDriverVersion("8.52.322.2201"); + // Windows 7 + gfxInfo.spoofOSVersion(0x60001); + break; + case "Linux": + // We don't support driver versions on Linux. + do_test_finished(); + return; + case "Darwin": + // We don't support driver versions on Darwin. + do_test_finished(); + return; + case "Android": + gfxInfo.spoofVendorID("abab"); + gfxInfo.spoofDeviceID("ghjk"); + gfxInfo.spoofDriverVersion("6"); + break; + } + + do_test_pending(); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8"); + await promiseStartupManager(); + + function checkBlacklist() { + var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + // Make sure unrelated features aren't affected + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + do_test_finished(); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + // If we wait until after we go through the event loop, gfxInfo is sure to + // have processed the gfxItems event. + executeSoon(checkBlacklist); + }, "blocklist-data-gfxItems"); + + mockGfxBlocklistItemsFromDisk("../data/test_gfxBlacklist.json"); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_OK.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_OK.js new file mode 100644 index 0000000000..2cc3686007 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_OK.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test whether a machine which exactly matches the greater-than-or-equal +// blacklist entry is successfully blocked. +// Uses test_gfxBlacklist.json + +// Performs the initial setup +async function run_test() { + var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + // We can't do anything if we can't spoof the stuff we need. + if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) { + do_test_finished(); + return; + } + + gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug); + + // Set the vendor/device ID, etc, to match the test file. + switch (Services.appinfo.OS) { + case "WINNT": + gfxInfo.spoofVendorID("0xabab"); + gfxInfo.spoofDeviceID("0x1234"); + gfxInfo.spoofDriverVersion("8.52.322.2202"); + // Windows 7 + gfxInfo.spoofOSVersion(0x60001); + break; + case "Linux": + // We don't support driver versions on Linux. + // XXX don't we? Seems like we do since bug 1294232 with the change in + // https://hg.mozilla.org/mozilla-central/diff/8962b8d9b7a6/widget/GfxInfoBase.cpp + do_test_finished(); + return; + case "Darwin": + // We don't support driver versions on Darwin. + do_test_finished(); + return; + case "Android": + gfxInfo.spoofVendorID("abab"); + gfxInfo.spoofDeviceID("ghjk"); + gfxInfo.spoofDriverVersion("7"); + break; + } + + do_test_pending(); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8"); + await promiseStartupManager(); + + function checkBlacklist() { + var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + + // Make sure unrelated features aren't affected + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + do_test_finished(); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + // If we wait until after we go through the event loop, gfxInfo is sure to + // have processed the gfxItems event. + executeSoon(checkBlacklist); + }, "blocklist-data-gfxItems"); + + mockGfxBlocklistItemsFromDisk("../data/test_gfxBlacklist.json"); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_No_Comparison.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_No_Comparison.js new file mode 100644 index 0000000000..169cdc5e62 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_No_Comparison.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test whether a machine which exactly matches the blacklist entry is +// successfully blocked. +// Uses test_gfxBlacklist.json + +// Performs the initial setup +async function run_test() { + var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + // We can't do anything if we can't spoof the stuff we need. + if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) { + do_test_finished(); + return; + } + + gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug); + + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x6666"); + + // Spoof the OS version so it matches the test file. + switch (Services.appinfo.OS) { + case "WINNT": + // Windows 7 + gfxInfo.spoofOSVersion(0x60001); + break; + case "Linux": + break; + case "Darwin": + gfxInfo.spoofOSVersion(0xa0900); + break; + case "Android": + break; + } + + do_test_pending(); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8"); + await promiseStartupManager(); + + function checkBlacklist() { + var driverVersion = gfxInfo.adapterDriverVersion; + if (driverVersion) { + var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DEVICE); + + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRENDER); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DEVICE); + + // Make sure unrelated features aren't affected + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS + ); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + } + do_test_finished(); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + // If we wait until after we go through the event loop, gfxInfo is sure to + // have processed the gfxItems event. + executeSoon(checkBlacklist); + }, "blocklist-data-gfxItems"); + + mockGfxBlocklistItemsFromDisk("../data/test_gfxBlacklist.json"); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OK.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OK.js new file mode 100644 index 0000000000..04d766e027 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OK.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test whether a machine which exactly matches the blacklist entry is +// successfully blocked. +// Uses test_gfxBlacklist.json + +// Performs the initial setup +async function run_test() { + var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + // We can't do anything if we can't spoof the stuff we need. + if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) { + do_test_finished(); + return; + } + + gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug); + + // Set the vendor/device ID, etc, to match the test file. + switch (Services.appinfo.OS) { + case "WINNT": + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x1234"); + gfxInfo.spoofDriverVersion("8.52.322.2201"); + // Windows 7 + gfxInfo.spoofOSVersion(0x60001); + break; + case "Linux": + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x1234"); + break; + case "Darwin": + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x1234"); + gfxInfo.spoofOSVersion(0xa0900); + break; + case "Android": + gfxInfo.spoofVendorID("abcd"); + gfxInfo.spoofDeviceID("asdf"); + gfxInfo.spoofDriverVersion("5"); + break; + } + + do_test_pending(); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8"); + await promiseStartupManager(); + + function checkBlacklist() { + var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + + // Make sure unrelated features aren't affected + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + do_test_finished(); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + // If we wait until after we go through the event loop, gfxInfo is sure to + // have processed the gfxItems event. + executeSoon(checkBlacklist); + }, "blocklist-data-gfxItems"); + + mockGfxBlocklistItemsFromDisk("../data/test_gfxBlacklist.json"); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OS.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OS.js new file mode 100644 index 0000000000..ce5a61cb75 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OS.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test whether a machine which differs only on OS version, but otherwise +// exactly matches the blacklist entry, is not blocked. +// Uses test_gfxBlacklist.json + +// Performs the initial setup +async function run_test() { + var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + // We can't do anything if we can't spoof the stuff we need. + if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) { + do_test_finished(); + return; + } + + gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug); + + // Set the vendor/device ID, etc, to match the test file. + switch (Services.appinfo.OS) { + case "WINNT": + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x1234"); + gfxInfo.spoofDriverVersion("8.52.322.2201"); + // Windows Vista + gfxInfo.spoofOSVersion(0x60000); + break; + case "Linux": + // We don't have any OS versions on Linux, just "Linux". + do_test_finished(); + return; + case "Darwin": + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x1234"); + gfxInfo.spoofOSVersion(0xa0800); + break; + case "Android": + // On Android, the driver version is used as the OS version (because + // there's so many of them). + do_test_finished(); + return; + } + + do_test_pending(); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8"); + await promiseStartupManager(); + + function checkBlacklist() { + var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + do_test_finished(); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + // If we wait until after we go through the event loop, gfxInfo is sure to + // have processed the gfxItems event. + executeSoon(checkBlacklist); + }, "blocklist-data-gfxItems"); + + mockGfxBlocklistItemsFromDisk("../data/test_gfxBlacklist.json"); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_match.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_match.js new file mode 100644 index 0000000000..7a4ec276ee --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_match.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test whether new OS versions are matched properly. +// Uses test_gfxBlacklist_OSVersion.json + +// Performs the initial setup +async function run_test() { + var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + // We can't do anything if we can't spoof the stuff we need. + if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) { + do_test_finished(); + return; + } + + gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug); + + // Set the vendor/device ID, etc, to match the test file. + gfxInfo.spoofDriverVersion("8.52.322.2201"); + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x1234"); + + // Spoof the version of the OS appropriately to test the test file. + switch (Services.appinfo.OS) { + case "WINNT": + // Windows 8 + gfxInfo.spoofOSVersion(0x60002); + break; + case "Linux": + // We don't have any OS versions on Linux, just "Linux". + do_test_finished(); + return; + case "Darwin": + // Mountain Lion + gfxInfo.spoofOSVersion(0xa0900); + break; + case "Android": + // On Android, the driver version is used as the OS version (because + // there's so many of them). + do_test_finished(); + return; + } + + do_test_pending(); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8"); + await promiseStartupManager(); + + function checkBlacklist() { + if (Services.appinfo.OS == "WINNT") { + var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + } else if (Services.appinfo.OS == "Darwin") { + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_OPENGL_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + } + + do_test_finished(); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + // If we wait until after we go through the event loop, gfxInfo is sure to + // have processed the gfxItems event. + executeSoon(checkBlacklist); + }, "blocklist-data-gfxItems"); + + mockGfxBlocklistItemsFromDisk("../data/test_gfxBlacklist_OSVersion.json"); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_DriverVersion.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_DriverVersion.js new file mode 100644 index 0000000000..61dba8db96 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_DriverVersion.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test whether blocklists specifying new OSes correctly don't block if driver +// versions are appropriately up-to-date. +// Uses test_gfxBlacklist_OSVersion.json + +// Performs the initial setup +async function run_test() { + var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + // We can't do anything if we can't spoof the stuff we need. + if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) { + do_test_finished(); + return; + } + + gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug); + + // Set the vendor/device ID, etc, to match the test file. + gfxInfo.spoofDriverVersion("8.52.322.2202"); + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x1234"); + + // Spoof the version of the OS appropriately to test the test file. + switch (Services.appinfo.OS) { + case "WINNT": + // Windows 8 + gfxInfo.spoofOSVersion(0x60002); + break; + case "Linux": + // We don't have any OS versions on Linux, just "Linux". + do_test_finished(); + return; + case "Darwin": + gfxInfo.spoofOSVersion(0xa0800); + break; + case "Android": + // On Android, the driver version is used as the OS version (because + // there's so many of them). + do_test_finished(); + return; + } + + do_test_pending(); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8"); + await promiseStartupManager(); + + function checkBlacklist() { + if (Services.appinfo.OS == "WINNT") { + var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + } else if (Services.appinfo.OS == "Darwin") { + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_OPENGL_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + } + + do_test_finished(); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + // If we wait until after we go through the event loop, gfxInfo is sure to + // have processed the gfxItems event. + executeSoon(checkBlacklist); + }, "blocklist-data-gfxItems"); + + mockGfxBlocklistItemsFromDisk("../data/test_gfxBlacklist_OSVersion.json"); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_OSVersion.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_OSVersion.js new file mode 100644 index 0000000000..117e2a34ee --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_OSVersion.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test whether old OS versions are not matched when the blacklist contains +// only new OS versions. +// Uses test_gfxBlacklist_OSVersion.json + +// Performs the initial setup +async function run_test() { + var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + // We can't do anything if we can't spoof the stuff we need. + if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) { + do_test_finished(); + return; + } + + gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug); + + // Set the vendor/device ID, etc, to match the test file. + gfxInfo.spoofDriverVersion("8.52.322.2201"); + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x1234"); + + // Spoof the version of the OS appropriately to test the test file. + switch (Services.appinfo.OS) { + case "WINNT": + // Windows 7 + gfxInfo.spoofOSVersion(0x60001); + break; + case "Linux": + // We don't have any OS versions on Linux, just "Linux". + do_test_finished(); + return; + case "Darwin": + // Lion + gfxInfo.spoofOSVersion(0xa0800); + break; + case "Android": + // On Android, the driver version is used as the OS version (because + // there's so many of them). + do_test_finished(); + return; + } + + do_test_pending(); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8"); + await promiseStartupManager(); + + function checkBlacklist() { + if (Services.appinfo.OS == "WINNT") { + var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + } else if (Services.appinfo.OS == "Darwin") { + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_OPENGL_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + } + + do_test_finished(); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + // If we wait until after we go through the event loop, gfxInfo is sure to + // have processed the gfxItems event. + executeSoon(checkBlacklist); + }, "blocklist-data-gfxItems"); + + mockGfxBlocklistItemsFromDisk("../data/test_gfxBlacklist_OSVersion.json"); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Vendor.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Vendor.js new file mode 100644 index 0000000000..37bc0d3c89 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Vendor.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test whether a machine which differs only on vendor, but otherwise +// exactly matches the blacklist entry, is not blocked. +// Uses test_gfxBlacklist.json + +// Performs the initial setup +async function run_test() { + var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + // We can't do anything if we can't spoof the stuff we need. + if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) { + do_test_finished(); + return; + } + + gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug); + + // Set the vendor/device ID, etc, to match the test file. + switch (Services.appinfo.OS) { + case "WINNT": + gfxInfo.spoofVendorID("0xdcba"); + gfxInfo.spoofDeviceID("0x1234"); + gfxInfo.spoofDriverVersion("8.52.322.2201"); + // Windows 7 + gfxInfo.spoofOSVersion(0x60001); + break; + case "Linux": + gfxInfo.spoofVendorID("0xdcba"); + gfxInfo.spoofDeviceID("0x1234"); + break; + case "Darwin": + gfxInfo.spoofVendorID("0xdcba"); + gfxInfo.spoofDeviceID("0x1234"); + gfxInfo.spoofOSVersion(0xa0900); + break; + case "Android": + gfxInfo.spoofVendorID("dcba"); + gfxInfo.spoofDeviceID("asdf"); + gfxInfo.spoofDriverVersion("5"); + break; + } + + do_test_pending(); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8"); + await promiseStartupManager(); + + function checkBlacklist() { + var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + do_test_finished(); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + // If we wait until after we go through the event loop, gfxInfo is sure to + // have processed the gfxItems event. + executeSoon(checkBlacklist); + }, "blocklist-data-gfxItems"); + + mockGfxBlocklistItemsFromDisk("../data/test_gfxBlacklist.json"); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Version.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Version.js new file mode 100644 index 0000000000..9a6a904465 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Version.js @@ -0,0 +1,190 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test whether a machine which exactly matches the blocklist entry is +// successfully blocked. +// Uses test_gfxBlacklist_AllOS.json + +// Performs the initial setup +async function run_test() { + var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + // We can't do anything if we can't spoof the stuff we need. + if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) { + do_test_finished(); + return; + } + + gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug); + + // Save OS in variable since createAppInfo below will change it to "xpcshell". + const OS = Services.appinfo.OS; + // Set the vendor/device ID, etc, to match the test file. + switch (OS) { + case "WINNT": + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x1234"); + gfxInfo.spoofDriverVersion("8.52.322.2201"); + // Windows 7 + gfxInfo.spoofOSVersion(0x60001); + break; + case "Linux": + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x1234"); + break; + case "Darwin": + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x1234"); + gfxInfo.spoofOSVersion(0xa0900); + break; + case "Android": + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x1234"); + gfxInfo.spoofDriverVersion("5"); + break; + } + + do_test_pending(); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "15.0", "8"); + await promiseStartupManager(); + + function checkBlocklist() { + var failureId = {}; + var status; + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_DIRECT2D, + failureId + ); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + Assert.equal(failureId.value, "FEATURE_FAILURE_DL_BLOCKLIST_g1"); + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS, + failureId + ); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + Assert.equal(failureId.value, "FEATURE_FAILURE_DL_BLOCKLIST_g2"); + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_DIRECT3D_10_LAYERS, + failureId + ); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + Assert.equal(failureId.value, ""); + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_DIRECT3D_10_1_LAYERS, + failureId + ); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + Assert.equal(failureId.value, ""); + + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_OPENGL_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_WEBGL_OPENGL, + failureId + ); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + Assert.equal(failureId.value, "FEATURE_FAILURE_DL_BLOCKLIST_g11"); + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_WEBGL_ANGLE, + failureId + ); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + Assert.equal(failureId.value, "FEATURE_FAILURE_DL_BLOCKLIST_NO_ID"); + + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBGL2, failureId); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + Assert.equal(failureId.value, "FEATURE_FAILURE_DL_BLOCKLIST_NO_ID"); + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_STAGEFRIGHT, + failureId + ); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_H264, + failureId + ); + if (OS == "Android" && status != Ci.nsIGfxInfo.FEATURE_STATUS_OK) { + // Hardware acceleration for H.264 varies by device. + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DEVICE); + Assert.equal(failureId.value, "FEATURE_FAILURE_WEBRTC_H264"); + } else { + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + } + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_ENCODE, + failureId + ); + if (OS == "Android" && status != Ci.nsIGfxInfo.FEATURE_STATUS_OK) { + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DEVICE); + Assert.equal(failureId.value, "FEATURE_FAILURE_WEBRTC_ENCODE"); + } else { + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + } + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_DECODE, + failureId + ); + if (OS == "Android" && status != Ci.nsIGfxInfo.FEATURE_STATUS_OK) { + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DEVICE); + Assert.equal(failureId.value, "FEATURE_FAILURE_WEBRTC_DECODE"); + } else { + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + } + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_DIRECT3D_11_LAYERS, + failureId + ); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_HARDWARE_VIDEO_DECODING, + failureId + ); + if (OS == "Linux" && status != Ci.nsIGfxInfo.FEATURE_STATUS_OK) { + // Linux test suite is running on SW OpenGL backend and we disable + // HW video decoding there. + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_PLATFORM_TEST); + Assert.equal( + failureId.value, + "FEATURE_FAILURE_VIDEO_DECODING_TEST_FAILED" + ); + } else { + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + } + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_DIRECT3D_11_ANGLE, + failureId + ); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + status = gfxInfo.getFeatureStatus( + Ci.nsIGfxInfo.FEATURE_DX_INTEROP2, + failureId + ); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + do_test_finished(); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + // If we wait until after we go through the event loop, gfxInfo is sure to + // have processed the gfxItems event. + executeSoon(checkBlocklist); + }, "blocklist-data-gfxItems"); + + mockGfxBlocklistItemsFromDisk("../data/test_gfxBlacklist_AllOS.json"); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_prefs.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_prefs.js new file mode 100644 index 0000000000..34e92b0e80 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_prefs.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test whether the blacklist successfully adds and removes the prefs that store +// its decisions when the remote blacklist is changed. +// Uses test_gfxBlacklist.json and test_gfxBlacklist2.json + +// Performs the initial setup +async function run_test() { + try { + var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + } catch (e) { + do_test_finished(); + return; + } + + // We can't do anything if we can't spoof the stuff we need. + if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) { + do_test_finished(); + return; + } + + gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug); + + // Set the vendor/device ID, etc, to match the test file. + switch (Services.appinfo.OS) { + case "WINNT": + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x1234"); + gfxInfo.spoofDriverVersion("8.52.322.2201"); + // Windows 7 + gfxInfo.spoofOSVersion(0x60001); + break; + case "Linux": + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x1234"); + break; + case "Darwin": + gfxInfo.spoofVendorID("0xabcd"); + gfxInfo.spoofDeviceID("0x1234"); + gfxInfo.spoofOSVersion(0xa0900); + break; + case "Android": + gfxInfo.spoofVendorID("abcd"); + gfxInfo.spoofDeviceID("asdf"); + gfxInfo.spoofDriverVersion("5"); + break; + } + + do_test_pending(); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8"); + await promiseStartupManager(); + + function blacklistAdded(aSubject, aTopic, aData) { + // If we wait until after we go through the event loop, gfxInfo is sure to + // have processed the gfxItems event. + executeSoon(ensureBlacklistSet); + } + function ensureBlacklistSet() { + var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); + + // Make sure unrelated features aren't affected + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + Assert.equal( + Services.prefs.getIntPref("gfx.blacklist.direct2d"), + Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION + ); + + Services.obs.removeObserver(blacklistAdded, "blocklist-data-gfxItems"); + Services.obs.addObserver(blacklistRemoved, "blocklist-data-gfxItems"); + mockGfxBlocklistItems([ + { + os: "WINNT 6.1", + vendor: "0xabcd", + devices: ["0x2783", "0x2782"], + feature: " DIRECT2D ", + featureStatus: " BLOCKED_DRIVER_VERSION ", + driverVersion: " 8.52.322.2202 ", + driverVersionComparator: " LESS_THAN ", + }, + { + os: "WINNT 6.0", + vendor: "0xdcba", + devices: ["0x2783", "0x1234", "0x2782"], + feature: " DIRECT3D_9_LAYERS ", + featureStatus: " BLOCKED_DRIVER_VERSION ", + driverVersion: " 8.52.322.2202 ", + driverVersionComparator: " LESS_THAN ", + }, + ]); + } + + function blacklistRemoved(aSubject, aTopic, aData) { + // If we wait until after we go through the event loop, gfxInfo is sure to + // have processed the gfxItems event. + executeSoon(ensureBlacklistUnset); + } + function ensureBlacklistUnset() { + var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + // Make sure unrelated features aren't affected + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS); + Assert.equal(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + var exists = false; + try { + Services.prefs.getIntPref("gfx.blacklist.direct2d"); + exists = true; + } catch (e) {} + + Assert.ok(!exists); + + do_test_finished(); + } + + Services.obs.addObserver(blacklistAdded, "blocklist-data-gfxItems"); + mockGfxBlocklistItemsFromDisk("../data/test_gfxBlacklist.json"); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_softblocked.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_softblocked.js new file mode 100644 index 0000000000..edf53183d0 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_softblocked.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// useMLBF=true only supports hard blocks, not soft blocks. +enable_blocklist_v2_instead_of_useMLBF(); + +// Tests that an appDisabled add-on that becomes softBlocked remains disabled +// when becoming appEnabled +add_task(async function test_softblock() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + await promiseStartupManager(); + + await promiseInstallWebExtension({ + manifest: { + name: "Softblocked add-on", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "softblock1@tests.mozilla.org", + strict_min_version: "2", + strict_max_version: "3", + }, + }, + }, + }); + let s1 = await promiseAddonByID("softblock1@tests.mozilla.org"); + + // Make sure to mark it as previously enabled. + await s1.enable(); + + Assert.ok(!s1.softDisabled); + Assert.ok(s1.appDisabled); + Assert.ok(!s1.isActive); + + await AddonTestUtils.loadBlocklistRawData({ + extensions: [ + { + guid: "softblock1@tests.mozilla.org", + versionRange: [ + { + severity: "1", + }, + ], + }, + ], + }); + + Assert.ok(s1.softDisabled); + Assert.ok(s1.appDisabled); + Assert.ok(!s1.isActive); + + AddonTestUtils.appInfo.platformVersion = "2"; + await promiseRestartManager("2"); + + s1 = await promiseAddonByID("softblock1@tests.mozilla.org"); + + Assert.ok(s1.softDisabled); + Assert.ok(!s1.appDisabled); + Assert.ok(!s1.isActive); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/xpcshell.toml b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/xpcshell.toml new file mode 100644 index 0000000000..2aee95e952 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/xpcshell.toml @@ -0,0 +1,102 @@ +[DEFAULT] +tags = "addons blocklist" +head = "head.js ../head_addons.js" +firefox-appdir = "browser" +support-files = [ + "../data/**", + "../../xpinstall/webmidi_permission.xpi", +] + +["test_android_blocklist_dump.js"] +run-if = ["os == 'android'"] + +["test_blocklist_addonBlockURL.js"] + +["test_blocklist_appversion.js"] +skip-if = ["os == 'android' && verify"] # times out + +["test_blocklist_clients.js"] +tags = "remote-settings" + +["test_blocklist_gfx.js"] + +["test_blocklist_metadata_filters.js"] + +["test_blocklist_mlbf.js"] + +["test_blocklist_mlbf_dump.js"] +skip-if = ["os == 'android'"] # blocklist v3 is not bundled with Android builds, see test_android_blocklist_dump.js instead. + +["test_blocklist_mlbf_fetch.js"] + +["test_blocklist_mlbf_stashes.js"] + +["test_blocklist_mlbf_telemetry.js"] +skip-if = ["appname == 'thunderbird'"] # Data irrelevant to Thunderbird. Bug 1641400. + +["test_blocklist_mlbf_update.js"] + +["test_blocklist_osabi.js"] +skip-if = ["os == 'android' && verify"] # times out + +["test_blocklist_prefs.js"] + +["test_blocklist_regexp_split.js"] + +["test_blocklist_severities.js"] + +["test_blocklist_statechange_telemetry.js"] +skip-if = ["appname == 'thunderbird'"] # Data irrelevant to Thunderbird. Bug 1641400. + +["test_blocklist_targetapp_filter.js"] +tags = "remote-settings" + +["test_blocklist_telemetry.js"] +tags = "remote-settings" +skip-if = ["appname == 'thunderbird'"] # Data irrelevant to Thunderbird. Bug 1641400. + +["test_blocklistchange.js"] +# Times out during parallel runs on desktop +requesttimeoutfactor = 2 +skip-if = ["os == 'android' && verify"] # times out because it takes too much time to run the full test + +["test_blocklistchange_v2.js"] +# Times out during parallel runs on desktop +requesttimeoutfactor = 2 +skip-if = ["os == 'android' && verify"] # times out in chaos mode on Android because several minutes are spent waiting at https://hg.mozilla.org/mozilla-central/file/3350b680/toolkit/mozapps/extensions/Blocklist.jsm#l698 + +["test_gfxBlacklist_Device.js"] + +["test_gfxBlacklist_DriverNew.js"] + +["test_gfxBlacklist_Equal_DriverNew.js"] + +["test_gfxBlacklist_Equal_DriverOld.js"] + +["test_gfxBlacklist_Equal_OK.js"] + +["test_gfxBlacklist_GTE_DriverOld.js"] + +["test_gfxBlacklist_GTE_OK.js"] + +["test_gfxBlacklist_No_Comparison.js"] + +["test_gfxBlacklist_OK.js"] + +["test_gfxBlacklist_OS.js"] + +["test_gfxBlacklist_OSVersion_match.js"] + +["test_gfxBlacklist_OSVersion_mismatch_DriverVersion.js"] + +["test_gfxBlacklist_OSVersion_mismatch_OSVersion.js"] + +["test_gfxBlacklist_Vendor.js"] + +["test_gfxBlacklist_Version.js"] + +["test_gfxBlacklist_prefs.js"] +# Bug 1248787 - consistently fails +skip-if = ["true"] + +["test_softblocked.js"] diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AMBrowserExtensionsImport.js b/toolkit/mozapps/extensions/test/xpcshell/test_AMBrowserExtensionsImport.js new file mode 100644 index 0000000000..b3416b227a --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_AMBrowserExtensionsImport.js @@ -0,0 +1,500 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const { AMBrowserExtensionsImport } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); + +const mockAddonRepository = ({ + addons = [], + expectedBrowserID = null, + expectedExtensionIDs = null, + matchedIDs = [], + unmatchedIDs = [], +}) => { + return { + async getMappedAddons(browserID, extensionIDs) { + if (expectedBrowserID) { + Assert.equal(browserID, expectedBrowserID, "expected browser ID"); + } + if (expectedExtensionIDs) { + Assert.deepEqual( + extensionIDs, + expectedExtensionIDs, + "expected extension IDs" + ); + } + + return Promise.resolve({ + addons, + matchedIDs, + unmatchedIDs, + }); + }, + }; +}; + +const assertStageInstallsResult = (result, importedAddonIDs) => { + // Sort the results to always assert the elements in the same order. + result.importedAddonIDs.sort(); + Assert.deepEqual(result, { importedAddonIDs }, "expected results"); + Assert.ok( + AMBrowserExtensionsImport.hasPendingImportedAddons, + "expected pending imported add-ons" + ); +}; + +const cancelInstalls = async importedAddonIDs => { + const promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-cancelled" + ); + // We want to verify that we received a `onInstallCancelled` event per + // (cancelled) install (i.e. per imported add-on). + const cancelledPromises = importedAddonIDs.map(id => + AddonTestUtils.promiseInstallEvent( + "onInstallCancelled", + (install, cancelledByUser) => { + Assert.equal(cancelledByUser, false, "Not user-cancelled"); + return install.addon.id == id; + } + ) + ); + await AMBrowserExtensionsImport.cancelInstalls(); + await Promise.all([promiseTopic, ...cancelledPromises]); + Assert.ok( + !AMBrowserExtensionsImport.hasPendingImportedAddons, + "expected no pending imported add-ons" + ); +}; + +const TEST_SERVER = createHttpServer({ hosts: ["example.com"] }); + +const ADDONS = { + ext1: { + manifest: { + name: "Ext 1", + version: "1.0", + browser_specific_settings: { gecko: { id: "ff@ext-1" } }, + }, + }, + ext2: { + manifest: { + name: "Ext 2", + version: "1.0", + browser_specific_settings: { gecko: { id: "ff@ext-2" } }, + }, + }, +}; +// Populated in `setup()`. +const XPIS = {}; +// Populated in `setup()`. +const ADDON_SEARCH_RESULTS = {}; + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +add_setup(async function setup() { + for (const [name, data] of Object.entries(ADDONS)) { + XPIS[name] = AddonTestUtils.createTempWebExtensionFile(data); + TEST_SERVER.registerFile(`/addons/${name}.xpi`, XPIS[name]); + + ADDON_SEARCH_RESULTS[name] = { + id: data.manifest.browser_specific_settings.gecko.id, + name: data.name, + version: data.version, + sourceURI: Services.io.newURI(`http://example.com/addons/${name}.xpi`), + icons: {}, + }; + } + + await AddonTestUtils.promiseStartupManager(); + + // FOG needs a profile directory to put its data in. + const profileDir = do_get_profile(); + + // FOG needs to be initialized in order for data to flow. + Services.fog.initializeFOG(); + + // When we stage installs and then cancel them, `XPIInstall` won't be able to + // remove the staging directory (which is expected to be empty) until the + // next restart. This causes an `AddonTestUtils` assertion to fail because we + // don't expect any staging directory at the end of the tests. That's why we + // remove this directory in the cleanup function defined below. + // + // We only remove the staging directory and that will only works if the + // directory is empty, otherwise an unchaught error will be thrown (on + // purpose). + registerCleanupFunction(() => { + const stagingDir = profileDir.clone(); + stagingDir.append("extensions"); + stagingDir.append("staged"); + stagingDir.exists() && stagingDir.remove(/* recursive */ false); + + // Clear the add-on repository override. + AMBrowserExtensionsImport._addonRepository = null; + }); +}); + +add_task(async function test_stage_and_complete_installs() { + const browserID = "some-browser-id"; + const extensionIDs = ["ext-1", "ext-2"]; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ + addons: Object.values(ADDON_SEARCH_RESULTS), + expectedBrowserID: browserID, + expectedExtensionIDs: extensionIDs, + }); + const importedAddonIDs = ["ff@ext-1", "ff@ext-2"]; + + let promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-pending" + ); + const result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + await promiseTopic; + assertStageInstallsResult(result, importedAddonIDs); + + // Make sure the prompt handler is the one from `AMBrowserExtensionsImport` + // since we don't want to show a permission prompt during an import. + for (const install of AMBrowserExtensionsImport._pendingInstallsMap.values()) { + Assert.equal( + install.promptHandler, + AMBrowserExtensionsImport._installPromptHandler, + "expected prompt handler to be the one set by AMBrowserExtensionsImport" + ); + } + + promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-complete" + ); + const endedPromises = importedAddonIDs.map(id => + AddonTestUtils.promiseInstallEvent( + "onInstallEnded", + install => install.addon.id == id + ) + ); + await AMBrowserExtensionsImport.completeInstalls(); + await Promise.all([promiseTopic, ...endedPromises]); + + Assert.ok( + !AMBrowserExtensionsImport.hasPendingImportedAddons, + "expected no pending imported add-ons" + ); + Assert.ok( + !AMBrowserExtensionsImport._canCompleteOrCancelInstalls && + !AMBrowserExtensionsImport._importInProgress, + "expected internal state to be consistent" + ); + + for (const id of importedAddonIDs) { + const addon = await AddonManager.getAddonByID(id); + Assert.ok(addon.isActive, `expected add-on "${id}" to be enabled`); + await addon.uninstall(); + } +}); + +add_task(async function test_stage_and_cancel_installs() { + const browserID = "some-browser-id"; + const extensionIDs = ["ext-1", "ext-2"]; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ + addons: Object.values(ADDON_SEARCH_RESULTS), + expectedBrowserID: browserID, + expectedExtensionIDs: extensionIDs, + }); + const importedAddonIDs = ["ff@ext-1", "ff@ext-2"]; + + const promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-pending" + ); + const result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + await promiseTopic; + assertStageInstallsResult(result, importedAddonIDs); + + await cancelInstalls(importedAddonIDs); +}); + +add_task(async function test_stageInstalls_telemetry() { + const browserID = "some-browser-id"; + const extensionIDs = ["ext-1", "ext-2"]; + const unmatchedIDs = ["unmatched-1", "unmatched-2"]; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ + addons: Object.values(ADDON_SEARCH_RESULTS), + expectedBrowserID: browserID, + expectedExtensionIDs: extensionIDs, + matchedIDs: ["ext-1", "ext-2"], + unmatchedIDs, + }); + const importedAddonIDs = ["ff@ext-1", "ff@ext-2"]; + + const promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-pending" + ); + const result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + await promiseTopic; + assertStageInstallsResult(result, importedAddonIDs); + + Assert.deepEqual( + Glean.browserMigration.matchedExtensions.testGetValue(), + extensionIDs + ); + Assert.deepEqual( + Glean.browserMigration.unmatchedExtensions.testGetValue(), + unmatchedIDs + ); + + await cancelInstalls(importedAddonIDs); +}); + +add_task(async function test_call_stageInstalls_twice() { + const browserID = "some-browser-id"; + const extensionIDs = ["ext-1"]; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ + // Only return one extension. + addons: Object.values(ADDON_SEARCH_RESULTS).slice(0, 1), + expectedBrowserID: browserID, + expectedExtensionIDs: extensionIDs, + }); + const importedAddonIDs = ["ff@ext-1"]; + + const promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-pending" + ); + let result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + await promiseTopic; + assertStageInstallsResult(result, importedAddonIDs); + + await Assert.rejects( + AMBrowserExtensionsImport.stageInstalls(browserID, []), + /Cannot stage installs because there are pending imported add-ons/, + "expected rejection because there are pending imported add-ons" + ); + + // Cancel the installs for the previous import. + await cancelInstalls(importedAddonIDs); + + // We should now be able to stage installs again. + result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + assertStageInstallsResult(result, importedAddonIDs); + + await cancelInstalls(importedAddonIDs); +}); + +add_task(async function test_call_stageInstalls_no_addons() { + const browserID = "some-browser-id"; + const extensionIDs = ["ext-123456"]; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ + // Returns no mapped add-ons. + addons: [], + expectedBrowserID: browserID, + expectedExtensionIDs: extensionIDs, + }); + + const result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + + Assert.deepEqual(result, { importedAddonIDs: [] }, "expected result"); + Assert.ok( + !AMBrowserExtensionsImport.hasPendingImportedAddons, + "expected no pending imported add-ons" + ); + Assert.ok( + !AMBrowserExtensionsImport._canCompleteOrCancelInstalls && + !AMBrowserExtensionsImport._importInProgress, + "expected internal state to be consistent" + ); +}); + +add_task(async function test_import_twice() { + const browserID = "some-browser-id"; + const extensionIDs = ["ext-1", "ext-2"]; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ + addons: Object.values(ADDON_SEARCH_RESULTS), + expectedBrowserID: browserID, + expectedExtensionIDs: extensionIDs, + }); + const importedAddonIDs = ["ff@ext-1", "ff@ext-2"]; + + let promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-pending" + ); + let result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + await promiseTopic; + assertStageInstallsResult(result, importedAddonIDs); + + // Finalize the installs. + promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-complete" + ); + const endedPromises = importedAddonIDs.map(id => + AddonTestUtils.promiseInstallEvent( + "onInstallEnded", + install => install.addon.id == id + ) + ); + await AMBrowserExtensionsImport.completeInstalls(); + await Promise.all([promiseTopic, ...endedPromises]); + + // Try to import the same add-ons again. Because these add-ons are already + // installed, we shouldn't re-import them again. + result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + Assert.deepEqual(result, { importedAddonIDs: [] }, "expected result"); + Assert.ok( + !AMBrowserExtensionsImport.hasPendingImportedAddons, + "expected no pending imported add-ons" + ); + Assert.ok( + !AMBrowserExtensionsImport._canCompleteOrCancelInstalls && + !AMBrowserExtensionsImport._importInProgress, + "expected internal state to be consistent" + ); + + for (const id of importedAddonIDs) { + const addon = await AddonManager.getAddonByID(id); + Assert.ok(addon.isActive, `expected add-on "${id}" to be enabled`); + await addon.uninstall(); + } +}); + +add_task(async function test_call_cancelInstalls_without_pending_import() { + await Assert.rejects( + AMBrowserExtensionsImport.cancelInstalls(), + /No import in progress/, + "expected an error" + ); +}); + +add_task(async function test_call_completeInstalls_without_pending_import() { + await Assert.rejects( + AMBrowserExtensionsImport.completeInstalls(), + /No import in progress/, + "expected an error" + ); +}); + +add_task(async function test_stage_installs_with_download_aborted() { + const browserID = "some-browser-id"; + const extensionIDs = ["ext-1", "ext-2"]; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ + addons: Object.values(ADDON_SEARCH_RESULTS), + expectedBrowserID: browserID, + expectedExtensionIDs: extensionIDs, + }); + const importedAddonIDs = ["ff@ext-2"]; + + // This listener will be triggered once (for the first imported add-on). Its + // goal is to cancel the download of an imported add-on and make sure it + // doesn't break everything. We still expect the second add-on to import to + // be staged for install. + const onNewInstall = AddonTestUtils.promiseInstallEvent( + "onNewInstall", + install => { + install.addListener({ + onDownloadStarted: () => false, + }); + return true; + } + ); + const promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-pending" + ); + const result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + await Promise.all([onNewInstall, promiseTopic]); + assertStageInstallsResult(result, importedAddonIDs); + + Assert.ok( + AMBrowserExtensionsImport.hasPendingImportedAddons, + "expected pending imported add-ons" + ); + Assert.ok( + AMBrowserExtensionsImport._canCompleteOrCancelInstalls && + AMBrowserExtensionsImport._importInProgress, + "expected internal state to be consistent" + ); + + // Let's cancel the pending installs. + await cancelInstalls(importedAddonIDs); +}); + +add_task(async function test_stageInstalls_then_restart_addonManager() { + const browserID = "some-browser-id"; + const extensionIDs = ["ext-1", "ext-2"]; + const EXPECTED_SOURCE_URI_SPECS = { + ["ff@ext-1"]: "http://example.com/addons/ext1.xpi", + ["ff@ext-2"]: "http://example.com/addons/ext2.xpi", + }; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ + addons: Object.values(ADDON_SEARCH_RESULTS), + expectedBrowserID: browserID, + expectedExtensionIDs: extensionIDs, + }); + const importedAddonIDs = ["ff@ext-1", "ff@ext-2"]; + + let promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-pending" + ); + let result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + await promiseTopic; + assertStageInstallsResult(result, importedAddonIDs); + + // We restart the add-ons manager to simulate a browser restart. It isn't + // quite the same but that should be enough. + await AddonTestUtils.promiseRestartManager(); + + for (const id of importedAddonIDs) { + const addon = await AddonManager.getAddonByID(id); + Assert.ok(addon.isActive, `expected add-on "${id}" to be enabled`); + // Verify that the sourceURI and installTelemetryInfo also match + // the values expected for the addons installed from the browser + // imports install flow. + Assert.deepEqual( + { + id: addon.id, + sourceURI: addon.sourceURI?.spec, + installTelemetryInfo: addon.installTelemetryInfo, + }, + { + id, + sourceURI: EXPECTED_SOURCE_URI_SPECS[id], + installTelemetryInfo: { + source: AMBrowserExtensionsImport.TELEMETRY_SOURCE, + }, + }, + "Got the expected AddonWrapper properties" + ); + await addon.uninstall(); + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AbuseReporter.js b/toolkit/mozapps/extensions/test/xpcshell/test_AbuseReporter.js new file mode 100644 index 0000000000..e5dffe0b00 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_AbuseReporter.js @@ -0,0 +1,908 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const { AbuseReporter, AbuseReportError } = ChromeUtils.importESModule( + "resource://gre/modules/AbuseReporter.sys.mjs" +); + +const { ClientID } = ChromeUtils.importESModule( + "resource://gre/modules/ClientID.sys.mjs" +); +const { TelemetryController } = ChromeUtils.importESModule( + "resource://gre/modules/TelemetryController.sys.mjs" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const APPNAME = "XPCShell"; +const APPVERSION = "1"; +const ADDON_ID = "test-addon@tests.mozilla.org"; +const ADDON_ID2 = "test-addon2@tests.mozilla.org"; +const FAKE_INSTALL_INFO = { + source: "fake-Install:Source", + method: "fake:install method", +}; +const PREF_REQUIRED_LOCALE = "intl.locale.requested"; +const REPORT_OPTIONS = { reportEntryPoint: "menu" }; +const TELEMETRY_EVENTS_FILTERS = { + category: "addonsManager", + method: "report", +}; + +const FAKE_AMO_DETAILS = { + name: { + "en-US": "fake name", + "it-IT": "fake it-IT name", + }, + current_version: { version: "1.0" }, + type: "extension", + is_recommended: true, +}; + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49"); + +const server = createHttpServer({ hosts: ["test.addons.org"] }); + +// Mock abuse report API endpoint. +let apiRequestHandler; +server.registerPathHandler("/api/report/", (request, response) => { + const stream = request.bodyInputStream; + const buffer = NetUtil.readInputStream(stream, stream.available()); + const data = new TextDecoder().decode(buffer); + apiRequestHandler({ data, request, response }); +}); + +// Mock addon details API endpoint. +const amoAddonDetailsMap = new Map(); +server.registerPrefixHandler("/api/addons/addon/", (request, response) => { + const addonId = request.path.split("/").pop(); + if (!amoAddonDetailsMap.has(addonId)) { + response.setStatusLine(request.httpVersion, 404, "Not Found"); + response.write(JSON.stringify({ detail: "Not found." })); + } else { + response.setStatusLine(request.httpVersion, 200, "Success"); + response.write(JSON.stringify(amoAddonDetailsMap.get(addonId))); + } +}); + +function getProperties(obj, propNames) { + return propNames.reduce((acc, el) => { + acc[el] = obj[el]; + return acc; + }, {}); +} + +function handleSubmitRequest({ request, response }) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json", false); + response.write("{}"); +} + +function clearAbuseReportState() { + // Clear the timestamp of the last submission. + AbuseReporter._lastReportTimestamp = null; +} + +async function installTestExtension(overrideOptions = {}) { + const extOptions = { + manifest: { + browser_specific_settings: { gecko: { id: ADDON_ID } }, + name: "Test Extension", + }, + useAddonManager: "permanent", + amInstallTelemetryInfo: FAKE_INSTALL_INFO, + ...overrideOptions, + }; + + const extension = ExtensionTestUtils.loadExtension(extOptions); + await extension.startup(); + + const addon = await AddonManager.getAddonByID(ADDON_ID); + + return { extension, addon }; +} + +async function assertRejectsAbuseReportError(promise, errorType, errorInfo) { + let error; + + await Assert.rejects( + promise, + err => { + // Log the actual error to make investigating test failures easier. + Cu.reportError(err); + error = err; + return err instanceof AbuseReportError; + }, + `Got an AbuseReportError` + ); + + equal(error.errorType, errorType, "Got the expected errorType"); + equal(error.errorInfo, errorInfo, "Got the expected errorInfo"); + ok( + error.message.includes(errorType), + "errorType should be included in the error message" + ); + if (errorInfo) { + ok( + error.message.includes(errorInfo), + "errorInfo should be included in the error message" + ); + } +} + +async function assertBaseReportData({ reportData, addon }) { + // Report properties related to addon metadata. + equal(reportData.addon, ADDON_ID, "Got expected 'addon'"); + equal( + reportData.addon_version, + addon.version, + "Got expected 'addon_version'" + ); + equal( + reportData.install_date, + addon.installDate.toISOString(), + "Got expected 'install_date' in ISO format" + ); + equal( + reportData.addon_install_origin, + addon.sourceURI.spec, + "Got expected 'addon_install_origin'" + ); + equal( + reportData.addon_install_source, + "fake_install_source", + "Got expected 'addon_install_source'" + ); + equal( + reportData.addon_install_method, + "fake_install_method", + "Got expected 'addon_install_method'" + ); + equal( + reportData.addon_signature, + "privileged", + "Got expected 'addon_signature'" + ); + + // Report properties related to the environment. + equal( + reportData.client_id, + await ClientID.getClientIdHash(), + "Got the expected 'client_id'" + ); + equal(reportData.app, APPNAME.toLowerCase(), "Got expected 'app'"); + equal(reportData.appversion, APPVERSION, "Got expected 'appversion'"); + equal( + reportData.lang, + Services.locale.appLocaleAsBCP47, + "Got expected 'lang'" + ); + equal( + reportData.operating_system, + AppConstants.platform, + "Got expected 'operating_system'" + ); + equal( + reportData.operating_system_version, + Services.sysinfo.getProperty("version"), + "Got expected 'operating_system_version'" + ); +} + +add_task(async function test_setup() { + Services.prefs.setCharPref( + "extensions.abuseReport.url", + "http://test.addons.org/api/report/" + ); + + Services.prefs.setCharPref( + "extensions.abuseReport.amoDetailsURL", + "http://test.addons.org/api/addons/addon" + ); + + await promiseStartupManager(); + // Telemetry test setup needed to ensure that the builtin events are defined + // and they can be collected and verified. + await TelemetryController.testSetup(); + + // This is actually only needed on Android, because it does not properly support unified telemetry + // and so, if not enabled explicitly here, it would make these tests to fail when running on a + // non-Nightly build. + const oldCanRecordBase = Services.telemetry.canRecordBase; + Services.telemetry.canRecordBase = true; + registerCleanupFunction(() => { + Services.telemetry.canRecordBase = oldCanRecordBase; + }); + + // Register a fake it-IT locale (used to test localized AMO details in some + // of the test case defined in this test file). + L10nRegistry.getInstance().registerSources([ + L10nFileSource.createMock( + "mock", + "app", + ["it-IT", "fr-FR"], + "resource://fake/locales/{locale}", + [] + ), + ]); +}); + +add_task(async function test_addon_report_data() { + info("Verify report property for a privileged extension"); + const { addon, extension } = await installTestExtension(); + const data = await AbuseReporter.getReportData(addon); + await assertBaseReportData({ reportData: data, addon }); + await extension.unload(); + + info("Verify 'addon_signature' report property for non privileged extension"); + AddonTestUtils.usePrivilegedSignatures = false; + const { addon: addon2, extension: extension2 } = await installTestExtension(); + const data2 = await AbuseReporter.getReportData(addon2); + equal( + data2.addon_signature, + "signed", + "Got expected 'addon_signature' for non privileged extension" + ); + await extension2.unload(); + + info("Verify 'addon_install_method' report property on temporary install"); + const { addon: addon3, extension: extension3 } = await installTestExtension({ + useAddonManager: "temporary", + }); + const data3 = await AbuseReporter.getReportData(addon3); + equal( + data3.addon_install_source, + "temporary_addon", + "Got expected 'addon_install_method' on temporary install" + ); + await extension3.unload(); +}); + +add_task(async function test_report_on_not_installed_addon() { + Services.telemetry.clearEvents(); + + // Make sure that the AMO addons details API endpoint is going to + // return a 404 status for the not installed addon. + amoAddonDetailsMap.delete(ADDON_ID); + + await assertRejectsAbuseReportError( + AbuseReporter.createAbuseReport(ADDON_ID, REPORT_OPTIONS), + "ERROR_ADDON_NOTFOUND" + ); + + TelemetryTestUtils.assertEvents( + [ + { + object: REPORT_OPTIONS.reportEntryPoint, + value: ADDON_ID, + extra: { error_type: "ERROR_AMODETAILS_NOTFOUND" }, + }, + { + object: REPORT_OPTIONS.reportEntryPoint, + value: ADDON_ID, + extra: { error_type: "ERROR_ADDON_NOTFOUND" }, + }, + ], + TELEMETRY_EVENTS_FILTERS + ); + + Services.telemetry.clearEvents(); +}); + +// This tests verifies how the addon installTelemetryInfo values are being +// normalized into the addon_install_source and addon_install_method +// expected by the API endpoint. +add_task(async function test_normalized_addon_install_source_and_method() { + async function assertAddonInstallMethod(amInstallTelemetryInfo, expected) { + const { addon, extension } = await installTestExtension({ + amInstallTelemetryInfo, + }); + const { + addon_install_method, + addon_install_source, + addon_install_source_url, + } = await AbuseReporter.getReportData(addon); + + Assert.deepEqual( + { + addon_install_method, + addon_install_source, + addon_install_source_url, + }, + { + addon_install_method: expected.method, + addon_install_source: expected.source, + addon_install_source_url: expected.sourceURL, + }, + `Got the expected report data for ${JSON.stringify( + amInstallTelemetryInfo + )}` + ); + await extension.unload(); + } + + // Array of testcases: the `test` property contains the installTelemetryInfo value + // and the `expect` contains the expected normalized values. + const TEST_CASES = [ + // Explicitly verify normalized values on missing telemetry info. + { + test: null, + expect: { source: null, method: null }, + }, + + // Verify expected normalized values for some common install telemetry info. + { + test: { source: "about:addons", method: "drag-and-drop" }, + expect: { source: "about_addons", method: "drag_and_drop" }, + }, + { + test: { source: "amo", method: "amWebAPI" }, + expect: { source: "amo", method: "amwebapi" }, + }, + { + test: { source: "app-profile", method: "sideload" }, + expect: { source: "app_profile", method: "sideload" }, + }, + { + test: { source: "distribution" }, + expect: { source: "distribution", method: null }, + }, + { + test: { + method: "installTrigger", + source: "test-host", + sourceURL: "http://host.triggered.install/example?test=1", + }, + expect: { + method: "installtrigger", + source: "test_host", + sourceURL: "http://host.triggered.install/example?test=1", + }, + }, + { + test: { + method: "link", + source: "unknown", + sourceURL: "https://another.host/installExtension?name=ext1", + }, + expect: { + method: "link", + source: "unknown", + sourceURL: "https://another.host/installExtension?name=ext1", + }, + }, + ]; + + for (const { expect, test } of TEST_CASES) { + await assertAddonInstallMethod(test, expect); + } +}); + +add_task(async function test_report_create_and_submit() { + Services.telemetry.clearEvents(); + + // Override the test api server request handler, to be able to + // intercept the submittions to the test api server. + let reportSubmitted; + apiRequestHandler = ({ data, request, response }) => { + reportSubmitted = JSON.parse(data); + handleSubmitRequest({ request, response }); + }; + + const { addon, extension } = await installTestExtension(); + + const reportEntryPoint = "menu"; + const report = await AbuseReporter.createAbuseReport(ADDON_ID, { + reportEntryPoint, + }); + + equal(report.addon, addon, "Got the expected addon property"); + equal( + report.reportEntryPoint, + reportEntryPoint, + "Got the expected reportEntryPoint" + ); + + const baseReportData = await AbuseReporter.getReportData(addon); + const reportProperties = { + message: "test message", + reason: "test-reason", + }; + + info("Submitting report"); + report.setMessage(reportProperties.message); + report.setReason(reportProperties.reason); + await report.submit(); + + const expectedEntries = Object.entries({ + report_entry_point: reportEntryPoint, + ...baseReportData, + ...reportProperties, + }); + + for (const [expectedKey, expectedValue] of expectedEntries) { + equal( + reportSubmitted[expectedKey], + expectedValue, + `Got the expected submitted value for "${expectedKey}"` + ); + } + + TelemetryTestUtils.assertEvents( + [ + { + object: reportEntryPoint, + value: ADDON_ID, + extra: { addon_type: "extension" }, + }, + ], + TELEMETRY_EVENTS_FILTERS + ); + + await extension.unload(); +}); + +add_task(async function test_error_recent_submit() { + Services.telemetry.clearEvents(); + clearAbuseReportState(); + + let reportSubmitted; + apiRequestHandler = ({ data, request, response }) => { + reportSubmitted = JSON.parse(data); + handleSubmitRequest({ request, response }); + }; + + const { extension } = await installTestExtension(); + const report = await AbuseReporter.createAbuseReport(ADDON_ID, { + reportEntryPoint: "uninstall", + }); + + const { extension: extension2 } = await installTestExtension({ + manifest: { + browser_specific_settings: { gecko: { id: ADDON_ID2 } }, + name: "Test Extension2", + }, + }); + const report2 = await AbuseReporter.createAbuseReport( + ADDON_ID2, + REPORT_OPTIONS + ); + + // Submit the two reports in fast sequence. + report.setReason("reason1"); + report2.setReason("reason2"); + await report.submit(); + await assertRejectsAbuseReportError(report2.submit(), "ERROR_RECENT_SUBMIT"); + equal( + reportSubmitted.reason, + "reason1", + "Server only received the data from the first submission" + ); + + TelemetryTestUtils.assertEvents( + [ + { + object: "uninstall", + value: ADDON_ID, + extra: { addon_type: "extension" }, + }, + { + object: REPORT_OPTIONS.reportEntryPoint, + value: ADDON_ID2, + extra: { + addon_type: "extension", + error_type: "ERROR_RECENT_SUBMIT", + }, + }, + ], + TELEMETRY_EVENTS_FILTERS + ); + + await extension.unload(); + await extension2.unload(); +}); + +add_task(async function test_submission_server_error() { + const { extension } = await installTestExtension(); + + async function testErrorCode({ + responseStatus, + responseText = "", + expectedErrorType, + expectedErrorInfo, + expectRequest = true, + }) { + info( + `Test expected AbuseReportError on response status "${responseStatus}"` + ); + Services.telemetry.clearEvents(); + clearAbuseReportState(); + + let requestReceived = false; + apiRequestHandler = ({ request, response }) => { + requestReceived = true; + response.setStatusLine(request.httpVersion, responseStatus, "Error"); + response.write(responseText); + }; + + const report = await AbuseReporter.createAbuseReport( + ADDON_ID, + REPORT_OPTIONS + ); + report.setReason("a-reason"); + const promiseSubmit = report.submit(); + if (typeof expectedErrorType === "string") { + // Assert a specific AbuseReportError errorType. + await assertRejectsAbuseReportError( + promiseSubmit, + expectedErrorType, + expectedErrorInfo + ); + } else { + // Assert on a given Error class. + await Assert.rejects(promiseSubmit, expectedErrorType); + } + equal( + requestReceived, + expectRequest, + `${expectRequest ? "" : "Not "}received a request as expected` + ); + + TelemetryTestUtils.assertEvents( + [ + { + object: REPORT_OPTIONS.reportEntryPoint, + value: ADDON_ID, + extra: { + addon_type: "extension", + error_type: + typeof expectedErrorType === "string" + ? expectedErrorType + : "ERROR_UNKNOWN", + }, + }, + ], + TELEMETRY_EVENTS_FILTERS + ); + } + + await testErrorCode({ + responseStatus: 500, + responseText: "A server error", + expectedErrorType: "ERROR_SERVER", + expectedErrorInfo: JSON.stringify({ + status: 500, + responseText: "A server error", + }), + }); + await testErrorCode({ + responseStatus: 404, + responseText: "Not found error", + expectedErrorType: "ERROR_CLIENT", + expectedErrorInfo: JSON.stringify({ + status: 404, + responseText: "Not found error", + }), + }); + // Test response with unexpected status code. + await testErrorCode({ + responseStatus: 604, + responseText: "An unexpected status code", + expectedErrorType: "ERROR_UNKNOWN", + expectedErrorInfo: JSON.stringify({ + status: 604, + responseText: "An unexpected status code", + }), + }); + // Test response status 200 with invalid json data. + await testErrorCode({ + responseStatus: 200, + expectedErrorType: /SyntaxError: JSON.parse/, + }); + + // Test on invalid url. + Services.prefs.setCharPref( + "extensions.abuseReport.url", + "invalid-protocol://abuse-report" + ); + await testErrorCode({ + expectedErrorType: "ERROR_NETWORK", + expectRequest: false, + }); + + await extension.unload(); +}); + +add_task(async function set_test_abusereport_url() { + Services.prefs.setCharPref( + "extensions.abuseReport.url", + "http://test.addons.org/api/report/" + ); +}); + +add_task(async function test_submission_aborting() { + Services.telemetry.clearEvents(); + clearAbuseReportState(); + + const { extension } = await installTestExtension(); + + // override the api request handler with one that is never going to reply. + let receivedRequestsCount = 0; + let resolvePendingResponses; + const waitToReply = new Promise( + resolve => (resolvePendingResponses = resolve) + ); + + const onRequestReceived = new Promise(resolve => { + apiRequestHandler = ({ request, response }) => { + response.processAsync(); + response.setStatusLine(request.httpVersion, 200, "OK"); + receivedRequestsCount++; + resolve(); + + // Keep the request pending until resolvePendingResponses have been + // called. + waitToReply.then(() => { + response.finish(); + }); + }; + }); + + const report = await AbuseReporter.createAbuseReport( + ADDON_ID, + REPORT_OPTIONS + ); + report.setReason("a-reason"); + const promiseResult = report.submit(); + + await onRequestReceived; + + Assert.greater( + receivedRequestsCount, + 0, + "Got the expected number of requests" + ); + Assert.strictEqual( + await Promise.race([promiseResult, Promise.resolve("pending")]), + "pending", + "Submission fetch request should still be pending" + ); + + report.abort(); + + await assertRejectsAbuseReportError(promiseResult, "ERROR_ABORTED_SUBMIT"); + + TelemetryTestUtils.assertEvents( + [ + { + object: REPORT_OPTIONS.reportEntryPoint, + value: ADDON_ID, + extra: { addon_type: "extension", error_type: "ERROR_ABORTED_SUBMIT" }, + }, + ], + TELEMETRY_EVENTS_FILTERS + ); + + await extension.unload(); + + // Unblock pending requests on the server request handler side, so that the + // test file can shutdown (otherwise the test run will be stuck after this + // task completed). + resolvePendingResponses(); +}); + +add_task(async function test_truncated_string_properties() { + const generateString = len => new Array(len).fill("a").join(""); + + const LONG_STRINGS_ADDON_ID = "addon-with-long-strings-props@mochi.test"; + const { extension } = await installTestExtension({ + manifest: { + name: generateString(400), + description: generateString(400), + browser_specific_settings: { gecko: { id: LONG_STRINGS_ADDON_ID } }, + }, + }); + + // Override the test api server request handler, to be able to + // intercept the properties actually submitted. + let reportSubmitted; + apiRequestHandler = ({ data, request, response }) => { + reportSubmitted = JSON.parse(data); + handleSubmitRequest({ request, response }); + }; + + const report = await AbuseReporter.createAbuseReport( + LONG_STRINGS_ADDON_ID, + REPORT_OPTIONS + ); + + report.setMessage("fake-message"); + report.setReason("fake-reason"); + await report.submit(); + + const expected = { + addon_name: generateString(255), + addon_summary: generateString(255), + }; + + Assert.deepEqual( + { + addon_name: reportSubmitted.addon_name, + addon_summary: reportSubmitted.addon_summary, + }, + expected, + "Got the long strings truncated as expected" + ); + + await extension.unload(); +}); + +add_task(async function test_report_recommended() { + const NON_RECOMMENDED_ADDON_ID = "non-recommended-addon@mochi.test"; + const RECOMMENDED_ADDON_ID = "recommended-addon@mochi.test"; + + const now = Date.now(); + const not_before = new Date(now - 3600000).toISOString(); + const not_after = new Date(now + 3600000).toISOString(); + + const { extension: nonRecommended } = await installTestExtension({ + manifest: { + name: "Fake non recommended addon", + browser_specific_settings: { gecko: { id: NON_RECOMMENDED_ADDON_ID } }, + }, + }); + + const { extension: recommended } = await installTestExtension({ + manifest: { + name: "Fake recommended addon", + browser_specific_settings: { gecko: { id: RECOMMENDED_ADDON_ID } }, + }, + files: { + "mozilla-recommendation.json": { + addon_id: RECOMMENDED_ADDON_ID, + states: ["recommended"], + validity: { not_before, not_after }, + }, + }, + }); + + // Override the test api server request handler, to be able to + // intercept the properties actually submitted. + let reportSubmitted; + apiRequestHandler = ({ data, request, response }) => { + reportSubmitted = JSON.parse(data); + handleSubmitRequest({ request, response }); + }; + + async function checkReportedSignature(addonId, expectedAddonSignature) { + clearAbuseReportState(); + const report = await AbuseReporter.createAbuseReport( + addonId, + REPORT_OPTIONS + ); + report.setMessage("fake-message"); + report.setReason("fake-reason"); + await report.submit(); + equal( + reportSubmitted.addon_signature, + expectedAddonSignature, + `Got the expected addon_signature for ${addonId}` + ); + } + + await checkReportedSignature(NON_RECOMMENDED_ADDON_ID, "signed"); + await checkReportedSignature(RECOMMENDED_ADDON_ID, "curated"); + + await nonRecommended.unload(); + await recommended.unload(); +}); + +add_task(async function test_query_amo_details() { + async function assertReportOnAMODetails({ + addonId, + addonType = "extension", + expectedReport, + } = {}) { + // Clear last report timestamp and any telemetry event recorded so far. + clearAbuseReportState(); + Services.telemetry.clearEvents(); + + const report = await AbuseReporter.createAbuseReport(addonId, { + reportEntryPoint: "menu", + }); + + let reportSubmitted; + apiRequestHandler = ({ data, request, response }) => { + reportSubmitted = JSON.parse(data); + handleSubmitRequest({ request, response }); + }; + + report.setMessage("fake message"); + report.setReason("reason1"); + await report.submit(); + + Assert.deepEqual( + expectedReport, + getProperties(reportSubmitted, Object.keys(expectedReport)), + "Got the expected report properties" + ); + + // Telemetry recorded for the successfully submitted report. + TelemetryTestUtils.assertEvents( + [ + { + object: "menu", + value: addonId, + extra: { addon_type: FAKE_AMO_DETAILS.type }, + }, + ], + TELEMETRY_EVENTS_FILTERS + ); + + clearAbuseReportState(); + } + + // Add the expected AMO addons details. + const addonId = "not-installed-addon@mochi.test"; + amoAddonDetailsMap.set(addonId, FAKE_AMO_DETAILS); + + // Test on the default en-US locale. + Services.prefs.setCharPref(PREF_REQUIRED_LOCALE, "en-US"); + let locale = Services.locale.appLocaleAsBCP47; + equal(locale, "en-US", "Got the expected app locale set"); + + let expectedReport = { + addon: addonId, + addon_name: FAKE_AMO_DETAILS.name[locale], + addon_version: FAKE_AMO_DETAILS.current_version.version, + addon_install_source: "not_installed", + addon_install_method: null, + addon_signature: "curated", + }; + + await assertReportOnAMODetails({ addonId, expectedReport }); + + // Test with a non-default locale also available in the AMO details. + Services.prefs.setCharPref(PREF_REQUIRED_LOCALE, "it-IT"); + locale = Services.locale.appLocaleAsBCP47; + equal(locale, "it-IT", "Got the expected app locale set"); + + expectedReport = { + ...expectedReport, + addon_name: FAKE_AMO_DETAILS.name[locale], + }; + await assertReportOnAMODetails({ addonId, expectedReport }); + + // Test with a non-default locale not available in the AMO details. + Services.prefs.setCharPref(PREF_REQUIRED_LOCALE, "fr-FR"); + locale = Services.locale.appLocaleAsBCP47; + equal(locale, "fr-FR", "Got the expected app locale set"); + + expectedReport = { + ...expectedReport, + // Fallbacks on en-US for non available locales. + addon_name: FAKE_AMO_DETAILS.name["en-US"], + }; + await assertReportOnAMODetails({ addonId, expectedReport }); + + Services.prefs.clearUserPref(PREF_REQUIRED_LOCALE); + + amoAddonDetailsMap.clear(); +}); + +add_task(async function test_statictheme_normalized_into_type_theme() { + const themeId = "not-installed-statictheme@mochi.test"; + amoAddonDetailsMap.set(themeId, { + ...FAKE_AMO_DETAILS, + type: "statictheme", + }); + + const report = await AbuseReporter.createAbuseReport(themeId, REPORT_OPTIONS); + + equal(report.addon.id, themeId, "Got a report for the expected theme id"); + equal(report.addon.type, "theme", "Got the expected addon type"); + + amoAddonDetailsMap.clear(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js new file mode 100644 index 0000000000..6f1c99eaa8 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js @@ -0,0 +1,488 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests AddonRepository.jsm + +var gServer = createHttpServer({ hosts: ["example.com"] }); + +const PREF_GETADDONS_BROWSEADDONS = "extensions.getAddons.browseAddons"; +const PREF_GETADDONS_BROWSESEARCHRESULTS = + "extensions.getAddons.search.browseURL"; +const PREF_GET_BROWSER_MAPPINGS = "extensions.getAddons.browserMappings.url"; + +const BASE_URL = "http://example.com"; +const DEFAULT_URL = "about:blank"; + +const ADDONS = [ + { + manifest: { + name: "XPI Add-on 1", + version: "1.1", + browser_specific_settings: { + gecko: { id: "test_AddonRepository_1@tests.mozilla.org" }, + }, + }, + }, + { + manifest: { + name: "XPI Add-on 2", + version: "1.2", + theme: {}, + browser_specific_settings: { + gecko: { id: "test_AddonRepository_2@tests.mozilla.org" }, + }, + }, + }, + { + manifest: { + name: "XPI Add-on 3", + version: "1.3", + theme: {}, + browser_specific_settings: { + gecko: { id: "test_AddonRepository_3@tests.mozilla.org" }, + }, + }, + }, +]; + +// Path to source URI of installing add-on +const INSTALL_URL2 = "/addons/test_AddonRepository_2.xpi"; +// Path to source URI of non-active add-on (state = STATE_AVAILABLE) +const INSTALL_URL3 = "/addons/test_AddonRepository_3.xpi"; + +// Properties of an individual add-on that should be checked +// Note: name is checked separately +var ADDON_PROPERTIES = [ + "id", + "type", + "version", + "creator", + "developers", + "description", + "fullDescription", + "iconURL", + "icons", + "screenshots", + "supportURL", + "contributionURL", + "averageRating", + "reviewCount", + "reviewURL", + "weeklyDownloads", + "dailyUsers", + "sourceURI", + "updateDate", + "amoListingURL", +]; + +// Results of getAddonsByIDs +var GET_RESULTS = [ + { + id: "test1@tests.mozilla.org", + type: "extension", + version: "1.1", + creator: { + name: "Test Creator 1", + url: BASE_URL + "/creator1.html", + }, + developers: [ + { + name: "Test Developer 1", + url: BASE_URL + "/developer1.html", + }, + ], + description: "Test Summary 1", + fullDescription: "Test Description 1", + iconURL: BASE_URL + "/icon1.png", + icons: { 32: BASE_URL + "/icon1.png" }, + screenshots: [ + { + url: BASE_URL + "/full1-1.png", + width: 400, + height: 300, + thumbnailURL: BASE_URL + "/thumbnail1-1.png", + thumbnailWidth: 200, + thumbnailHeight: 150, + caption: "Caption 1 - 1", + }, + { + url: BASE_URL + "/full2-1.png", + thumbnailURL: BASE_URL + "/thumbnail2-1.png", + caption: "Caption 2 - 1", + }, + ], + supportURL: BASE_URL + "/support1.html", + contributionURL: BASE_URL + "/contribution1.html", + averageRating: 4, + reviewCount: 1111, + reviewURL: BASE_URL + "/review1.html", + weeklyDownloads: 3333, + sourceURI: BASE_URL + INSTALL_URL2, + updateDate: new Date(1265033045000), + amoListingURL: + "https://addons.mozilla.org/en-US/firefox/addon/test1@tests.mozilla.org/", + }, + { + id: "test2@tests.mozilla.org", + type: "extension", + version: "2.0", + icons: {}, + sourceURI: "http://example.com/addons/bleah.xpi", + }, + { + id: "test_AddonRepository_1@tests.mozilla.org", + type: "theme", + version: "1.4", + icons: {}, + }, +]; + +// Values for testing AddonRepository.getAddonsByIDs() +var GET_TEST = { + preference: PREF_GETADDONS_BYIDS, + preferenceValue: BASE_URL + "/%OS%/%VERSION%/%IDS%", + failedIDs: ["test1@tests.mozilla.org"], + failedURL: "/XPCShell/1/test1%40tests.mozilla.org", + successfulIDs: [ + "test1@tests.mozilla.org", + "test2@tests.mozilla.org", + "{00000000-1111-2222-3333-444444444444}", + "test_AddonRepository_1@tests.mozilla.org", + ], + successfulURL: + "/XPCShell/1/test1%40tests.mozilla.org%2C" + + "test2%40tests.mozilla.org%2C" + + "%7B00000000-1111-2222-3333-444444444444%7D%2C" + + "test_AddonRepository_1%40tests.mozilla.org", + successfulRTAURL: + "/XPCShell/1/rta%3AdGVzdDFAdGVzdHMubW96aWxsYS5vcmc%2C" + + "test2%40tests.mozilla.org%2C" + + "%7B00000000-1111-2222-3333-444444444444%7D%2C" + + "test_AddonRepository_1%40tests.mozilla.org", +}; + +const GET_BROWSER_MAPPINGS_URL = `${BASE_URL}/browser-mappings/%BROWSER%`; + +// Test that actual results and expected results are equal +function check_results(aActualAddons, aExpectedAddons) { + do_check_addons(aActualAddons, aExpectedAddons, ADDON_PROPERTIES); + + // Additional tests + aActualAddons.forEach(function check_each_addon(aActualAddon) { + // Separately check name so better messages are output when test fails + if (aActualAddon.name == "FAIL") { + do_throw(aActualAddon.id + " - " + aActualAddon.description); + } + if (aActualAddon.name != "PASS") { + do_throw(aActualAddon.id + " - invalid add-on name " + aActualAddon.name); + } + }); +} + +add_task(async function setup() { + // Setup for test + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9"); + + let xpis = ADDONS.map(addon => createTempWebExtensionFile(addon)); + + // Register other add-on XPI files + gServer.registerFile(INSTALL_URL2, xpis[1]); + gServer.registerFile(INSTALL_URL3, xpis[2]); + + // Register files used to test search failure + gServer.registerFile( + GET_TEST.failedURL, + do_get_file("data/test_AddonRepository_fail.json") + ); + + // Register files used to test search success + gServer.registerFile( + GET_TEST.successfulURL, + do_get_file("data/test_AddonRepository_getAddonsByIDs.json") + ); + // Register file for RTA test + gServer.registerFile( + GET_TEST.successfulRTAURL, + do_get_file("data/test_AddonRepository_getAddonsByIDs.json") + ); + + // Register some files/handlers for browser mapping tests. + gServer.registerFile( + // Keep in sync with `GET_BROWSER_MAPPINGS_URL`. + "/browser-mappings/valid-browser-id", + do_get_file("data/test_AddonRepository_getMappedAddons.json") + ); + gServer.registerFile( + // This is used in `test_getMappedAddons_empty_mapping()` and should be + // updated if `GET_BROWSER_MAPPINGS_URL` is also updated. + "/browser-mappings/browser-id-empty-results", + do_get_file("data/test_AddonRepository_getMappedAddons_empty.json") + ); + gServer.registerPrefixHandler( + // Keep in sync with the pref set in `test_getMappedAddons_with_paging()`. + "/browser-mappings/with-paging/valid-browser-id/", + // This handler parses the query string of the request it receives in order + // to force the `getMappedAddons()` method to call the same API endpoint a + // few times (by incrementing the integer value in the query string every + // time). After that, this handler returns a "next' URL that points to one + // of the valid endpoints registered above, which won't have a "next" URL. + // We do this to verify that `getMappedAddons()` supports paginated API + // results. + (request, response) => { + const page = parseInt(request.queryString, 10); + const nextPath = + page < 3 + ? `with-paging/valid-browser-id/?${page + 1}` + : `valid-browser-id`; + + response.setHeader("content-type", "application/json"); + response.write( + JSON.stringify({ + count: 0, + next: `${BASE_URL}/browser-mappings/${nextPath}`, + page_count: page, + page_size: 1, + previous: null, + results: [], + }) + ); + } + ); + + await promiseStartupManager(); + + // Install an add-on so can check that it isn't returned in the results + await promiseInstallFile(xpis[0]); + await promiseRestartManager(); + + // Create an active AddonInstall so can check that it isn't returned in the results + let install = await AddonManager.getInstallForURL(BASE_URL + INSTALL_URL2); + let promise = promiseCompleteInstall(install); + registerCleanupFunction(() => promise); + + // Create a non-active AddonInstall so can check that it is returned in the results + await AddonManager.getInstallForURL(BASE_URL + INSTALL_URL3); +}); + +// Tests homepageURL and getSearchURL() +add_task(async function test_1() { + function check_urls(aPreference, aGetURL, aTests) { + aTests.forEach(function (aTest) { + Services.prefs.setCharPref(aPreference, aTest.preferenceValue); + Assert.equal(aGetURL(aTest), aTest.expectedURL); + }); + } + + var urlTests = [ + { + preferenceValue: BASE_URL, + expectedURL: BASE_URL, + }, + { + preferenceValue: BASE_URL + "/%OS%/%VERSION%", + expectedURL: BASE_URL + "/XPCShell/1", + }, + ]; + + // Extra tests for AddonRepository.getSearchURL(); + var searchURLTests = [ + { + searchTerms: "test", + preferenceValue: BASE_URL + "/search?q=%TERMS%", + expectedURL: BASE_URL + "/search?q=test", + }, + { + searchTerms: "test search", + preferenceValue: BASE_URL + "/%TERMS%", + expectedURL: BASE_URL + "/test%20search", + }, + { + searchTerms: 'odd=search:with&weird"characters', + preferenceValue: BASE_URL + "/%TERMS%", + expectedURL: BASE_URL + "/odd%3Dsearch%3Awith%26weird%22characters", + }, + ]; + + // Setup tests for homepageURL and getSearchURL() + var tests = [ + { + initiallyUndefined: true, + preference: PREF_GETADDONS_BROWSEADDONS, + urlTests, + getURL: () => AddonRepository.homepageURL, + }, + { + initiallyUndefined: false, + preference: PREF_GETADDONS_BROWSESEARCHRESULTS, + urlTests: urlTests.concat(searchURLTests), + getURL: function getSearchURL(aTest) { + var searchTerms = + aTest && aTest.searchTerms ? aTest.searchTerms : "unused terms"; + return AddonRepository.getSearchURL(searchTerms); + }, + }, + ]; + + tests.forEach(function url_test(aTest) { + if (aTest.initiallyUndefined) { + // Preference is not defined by default + Assert.equal( + Services.prefs.getPrefType(aTest.preference), + Services.prefs.PREF_INVALID + ); + Assert.equal(aTest.getURL(), DEFAULT_URL); + } + + check_urls(aTest.preference, aTest.getURL, aTest.urlTests); + }); +}); + +// Tests failure of AddonRepository.getAddonsByIDs() +add_task(async function test_getAddonsByID_fails() { + Services.prefs.setCharPref(GET_TEST.preference, GET_TEST.preferenceValue); + + await Assert.rejects( + AddonRepository.getAddonsByIDs(GET_TEST.failedIDs), + /Error: GET.*?failed/ + ); +}); + +// Tests success of AddonRepository.getAddonsByIDs() +add_task(async function test_getAddonsByID_succeeds() { + let result = await AddonRepository.getAddonsByIDs(GET_TEST.successfulIDs); + + check_results(result, GET_RESULTS); +}); + +// Tests success of AddonRepository.getAddonsByIDs() with rta ID. +add_task(async function test_getAddonsByID_rta() { + let id = `rta:${btoa(GET_TEST.successfulIDs[0])}`.slice(0, -1); + GET_TEST.successfulIDs[0] = id; + let result = await AddonRepository.getAddonsByIDs(GET_TEST.successfulIDs); + + check_results(result, GET_RESULTS); +}); + +add_task( + { + pref_set: [[PREF_GET_BROWSER_MAPPINGS, GET_BROWSER_MAPPINGS_URL]], + }, + async function test_getMappedAddons() { + const { + addons: result, + matchedIDs, + unmatchedIDs, + } = await AddonRepository.getMappedAddons("valid-browser-id", [ + "browser-extension-test-1", + "browser-extension-test-2", + // This one is mapped but the search API won't return any data. + "browser-extension-test-3", + "browser-extension-test-4", + // These ones are not mapped to any Firefox add-ons. + "browser-extension-test-5", + "browser-extension-test-6", + ]); + Assert.equal(result.length, 3, "expected 3 mapped add-ons"); + check_results(result, GET_RESULTS); + Assert.deepEqual(matchedIDs, [ + "browser-extension-test-1", + "browser-extension-test-2", + "browser-extension-test-3", + "browser-extension-test-4", + ]); + Assert.deepEqual(unmatchedIDs, [ + "browser-extension-test-5", + "browser-extension-test-6", + ]); + } +); + +add_task( + { + pref_set: [[PREF_GET_BROWSER_MAPPINGS, GET_BROWSER_MAPPINGS_URL]], + }, + async function test_getMappedAddons_empty_list_of_ids() { + const { + addons: result, + matchedIDs, + unmatchedIDs, + } = await AddonRepository.getMappedAddons("valid-browser-id", []); + Assert.equal(result.length, 0, "expected 0 mapped add-ons"); + Assert.equal(matchedIDs.length, 0, "expected 0 matched IDs"); + Assert.equal(unmatchedIDs.length, 0, "expected 0 unmatched IDs"); + } +); + +add_task( + { + pref_set: [[PREF_GET_BROWSER_MAPPINGS, GET_BROWSER_MAPPINGS_URL]], + }, + async function test_getMappedAddons_invalid_ids() { + const { + addons: result, + matchedIDs, + unmatchedIDs, + } = await AddonRepository.getMappedAddons("valid-browser-id", [ + "", + null, + undefined, + ]); + Assert.equal(result.length, 0, "expected 0 mapped add-ons"); + Assert.equal(matchedIDs.length, 0, "expected 0 matched IDs"); + Assert.deepEqual(unmatchedIDs, ["", null, undefined]); + } +); + +add_task( + { + pref_set: [[PREF_GET_BROWSER_MAPPINGS, GET_BROWSER_MAPPINGS_URL]], + }, + async function test_getMappedAddons_empty_mapping() { + const { + addons: result, + matchedIDs, + unmatchedIDs, + } = await AddonRepository.getMappedAddons("browser-id-empty-results", [ + "browser-extension-test-1", + "browser-extension-test-2", + "browser-extension-test-3", + ]); + Assert.equal(result.length, 0, "expected no mapped add-ons"); + Assert.equal(matchedIDs.length, 0, "expected 0 matched IDs"); + Assert.equal(unmatchedIDs.length, 3, "expected 3 unmatched IDs"); + } +); + +add_task( + { + pref_set: [ + [ + PREF_GET_BROWSER_MAPPINGS, + `${BASE_URL}/browser-mappings/with-paging/%BROWSER%/?1`, + ], + ], + }, + async function test_getMappedAddons_with_paging() { + const { + addons: result, + matchedIDs, + unmatchedIDs, + } = await AddonRepository.getMappedAddons("valid-browser-id", [ + "browser-extension-test-1", + "browser-extension-test-2", + // This one is mapped but the search API won't return any data. + "browser-extension-test-3", + "browser-extension-test-4", + ]); + Assert.equal(result.length, 3, "expected 3 mapped add-ons"); + check_results(result, GET_RESULTS); + Assert.deepEqual(matchedIDs, [ + "browser-extension-test-1", + "browser-extension-test-2", + "browser-extension-test-3", + "browser-extension-test-4", + ]); + Assert.deepEqual(unmatchedIDs, []); + } +); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_appIsShuttingDown.js b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_appIsShuttingDown.js new file mode 100644 index 0000000000..4f23026ed3 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_appIsShuttingDown.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests AddonRepository.jsm when backgroundUpdateChecks are hit while the application +// shutdown has been already initiated (See Bug 1841444). + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +add_setup(async () => { + AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "1" + ); + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function test_backgroundUpdateCheck_after_shutdown_initiated() { + const sandbox = sinon.createSandbox(); + ok( + !AddonRepository.appIsShuttingDown, + "Expect real appIsShuttingDown getter to be returning false" + ); + sandbox.stub(AddonRepository, "appIsShuttingDown").get(() => true); + ok( + AddonRepository.appIsShuttingDown, + "Expect mocked appIsShuttingDown getter to be returning true" + ); + sandbox.spy(AddonRepository, "_getAllInstalledAddons"); + equal( + AddonRepository._getAllInstalledAddons.callCount, + 0, + "Expect _getAllInstalledAddons callCount to be initially 0" + ); + + await AddonRepository.backgroundUpdateCheck(); + + // We expect backgroundUpdateCheck to be returning earlier and not be calling _getAllInstalledAddons method at all. + equal( + AddonRepository._getAllInstalledAddons.callCount, + 0, + "Expect _getAllInstalledAddons to not have been called" + ); + sandbox.restore(); +}); + +add_task(async function test_fetchPaged_after_shutdown_initiated() { + const sandbox = sinon.createSandbox(); + ok( + !AddonRepository.appIsShuttingDown, + "Expect real appIsShuttingDown getter to be returning false" + ); + sandbox.stub(AddonRepository, "appIsShuttingDown").get(() => true); + ok( + AddonRepository.appIsShuttingDown, + "Expect mocked appIsShuttingDown getter to be returning true" + ); + sandbox.spy(AddonRepository, "_createServiceRequest"); + equal( + AddonRepository._createServiceRequest.callCount, + 0, + "Expect _createServiceRequest callCount to be initially 0" + ); + + await Assert.rejects( + AddonRepository.getAddonsByIDs(["ext01@testext", "ext02@testext"]), + /Reject ServiceRequest for ".*", shutdown already in progress/, + "Expect getAddonsByIds to reject when called after shutdown was already initiated" + ); + + // We expect backgroundUpdateCheck to be returning earlier and not be calling _getAllInstalledAddons method at all. + equal( + AddonRepository._createServiceRequest.callCount, + 0, + "Expect _createServiceRequest to not have been called" + ); + sandbox.restore(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js new file mode 100644 index 0000000000..2a1bc2721b --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js @@ -0,0 +1,728 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests caching in AddonRepository.sys.mjs. + +var gServer; + +const HOST = "example.com"; +const BASE_URL = "http://example.com"; + +const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled"; +const PREF_GETADDONS_CACHE_TYPES = "extensions.getAddons.cache.types"; +const GETADDONS_RESULTS = BASE_URL + "/data/test_AddonRepository_cache.json"; +const EMPTY_RESULT = BASE_URL + "/data/test_AddonRepository_empty.json"; +const FAILED_RESULT = BASE_URL + "/data/test_AddonRepository_fail.json"; + +const FILE_DATABASE = "addons.json"; + +const ADDONS = [ + { + manifest: { + name: "XPI Add-on 1", + version: "1.1", + + description: "XPI Add-on 1 - Description", + developer: { + name: "XPI Add-on 1 - Author", + }, + + homepage_url: "http://example.com/xpi/1/homepage.html", + icons: { + 32: "icon.png", + }, + + options_ui: { + page: "options.html", + }, + + browser_specific_settings: { + gecko: { id: "test_AddonRepository_1@tests.mozilla.org" }, + }, + }, + }, + { + manifest: { + name: "XPI Add-on 2", + version: "1.2", + theme: {}, + browser_specific_settings: { + gecko: { id: "test_AddonRepository_2@tests.mozilla.org" }, + }, + }, + }, + { + manifest: { + name: "XPI Add-on 3", + version: "1.3", + icons: { + 32: "icon.png", + }, + theme: {}, + browser_specific_settings: { + gecko: { id: "test_AddonRepository_3@tests.mozilla.org" }, + }, + }, + files: { + "preview.png": "", + }, + }, +]; + +const ADDON_IDS = ADDONS.map( + addon => addon.manifest.browser_specific_settings.gecko.id +); +const ADDON_FILES = ADDONS.map(addon => + AddonTestUtils.createTempWebExtensionFile(addon) +); + +const PREF_ADDON0_CACHE_ENABLED = + "extensions." + ADDON_IDS[0] + ".getAddons.cache.enabled"; +const PREF_ADDON1_CACHE_ENABLED = + "extensions." + ADDON_IDS[1] + ".getAddons.cache.enabled"; + +// Properties of an individual add-on that should be checked +// Note: updateDate is checked separately +const ADDON_PROPERTIES = [ + "id", + "type", + "name", + "version", + "developers", + "description", + "fullDescription", + "icons", + "screenshots", + "homepageURL", + "supportURL", + "optionsURL", + "averageRating", + "reviewCount", + "reviewURL", + "weeklyDownloads", + "sourceURI", +]; + +// The updateDate property is annoying to test for XPI add-ons. +// However, since we only care about whether the repository value vs. the +// XPI value is used, we can just test if the property value matches +// the repository value +const REPOSITORY_UPDATEDATE = 9; + +// Get the URI of a subfile locating directly in the folder of +// the add-on corresponding to the specified id +function get_subfile_uri(aId, aFilename) { + let file = gProfD.clone(); + file.append("extensions"); + return do_get_addon_root_uri(file, aId) + aFilename; +} + +// Expected repository add-ons +const REPOSITORY_ADDONS = [ + { + id: ADDON_IDS[0], + type: "extension", + name: "Repo Add-on 1", + version: "2.1", + developers: [ + { + name: "Repo Add-on 1 - First Developer", + url: BASE_URL + "/repo/1/firstDeveloper.html", + }, + { + name: "Repo Add-on 1 - Second Developer", + url: BASE_URL + "/repo/1/secondDeveloper.html", + }, + ], + description: "Repo Add-on 1 - Description\nSecond line", + fullDescription: "Repo Add-on 1 - Full Description & some extra", + icons: { 32: BASE_URL + "/repo/1/icon.png" }, + homepageURL: BASE_URL + "/repo/1/homepage.html", + supportURL: BASE_URL + "/repo/1/support.html", + averageRating: 1, + reviewCount: 1111, + reviewURL: BASE_URL + "/repo/1/review.html", + weeklyDownloads: 3331, + sourceURI: BASE_URL + "/repo/1/install.xpi", + }, + { + id: ADDON_IDS[1], + type: "theme", + name: "Repo Add-on 2", + version: "2.2", + developers: [ + { + name: "Repo Add-on 2 - First Developer", + url: BASE_URL + "/repo/2/firstDeveloper.html", + }, + { + name: "Repo Add-on 2 - Second Developer", + url: BASE_URL + "/repo/2/secondDeveloper.html", + }, + ], + description: "Repo Add-on 2 - Description", + fullDescription: "Repo Add-on 2 - Full Description", + icons: { 32: BASE_URL + "/repo/2/icon.png" }, + screenshots: [ + { + url: BASE_URL + "/repo/2/firstFull.png", + thumbnailURL: BASE_URL + "/repo/2/firstThumbnail.png", + caption: "Repo Add-on 2 - First Caption", + }, + { + url: BASE_URL + "/repo/2/secondFull.png", + thumbnailURL: BASE_URL + "/repo/2/secondThumbnail.png", + caption: "Repo Add-on 2 - Second Caption", + }, + ], + homepageURL: BASE_URL + "/repo/2/homepage.html", + supportURL: BASE_URL + "/repo/2/support.html", + averageRating: 2, + reviewCount: 1112, + reviewURL: BASE_URL + "/repo/2/review.html", + weeklyDownloads: 3332, + sourceURI: BASE_URL + "/repo/2/install.xpi", + }, + { + id: ADDON_IDS[2], + type: "theme", + name: "Repo Add-on 3", + version: "2.3", + icons: { 32: BASE_URL + "/repo/3/icon.png" }, + screenshots: [ + { + url: BASE_URL + "/repo/3/firstFull.png", + thumbnailURL: BASE_URL + "/repo/3/firstThumbnail.png", + caption: "Repo Add-on 3 - First Caption", + }, + { + url: BASE_URL + "/repo/3/secondFull.png", + thumbnailURL: BASE_URL + "/repo/3/secondThumbnail.png", + caption: "Repo Add-on 3 - Second Caption", + }, + ], + }, +]; + +function extensionURL(id, path) { + return WebExtensionPolicy.getByID(id).getURL(path); +} + +// Expected add-ons when not using cache +const WITHOUT_CACHE = [ + { + id: ADDON_IDS[0], + type: "extension", + name: "XPI Add-on 1", + version: "1.1", + authors: [{ name: "XPI Add-on 1 - Author" }], + description: "XPI Add-on 1 - Description", + get icons() { + return { 32: get_subfile_uri(ADDON_IDS[0], "icon.png") }; + }, + homepageURL: `${BASE_URL}/xpi/1/homepage.html`, + get optionsURL() { + return extensionURL(ADDON_IDS[0], "options.html"); + }, + sourceURI: NetUtil.newURI(ADDON_FILES[0]).spec, + }, + { + id: ADDON_IDS[1], + type: "theme", + name: "XPI Add-on 2", + version: "1.2", + sourceURI: NetUtil.newURI(ADDON_FILES[1]).spec, + icons: {}, + }, + { + id: ADDON_IDS[2], + type: "theme", + name: "XPI Add-on 3", + version: "1.3", + get icons() { + return { 32: get_subfile_uri(ADDON_IDS[2], "icon.png") }; + }, + screenshots: [ + { + get url() { + return get_subfile_uri(ADDON_IDS[2], "preview.png"); + }, + }, + ], + sourceURI: NetUtil.newURI(ADDON_FILES[2]).spec, + }, +]; + +// Expected add-ons when using cache +const WITH_CACHE = [ + { + id: ADDON_IDS[0], + type: "extension", + name: "XPI Add-on 1", + version: "1.1", + developers: [ + { + name: "Repo Add-on 1 - First Developer", + url: BASE_URL + "/repo/1/firstDeveloper.html", + }, + { + name: "Repo Add-on 1 - Second Developer", + url: BASE_URL + "/repo/1/secondDeveloper.html", + }, + ], + description: "XPI Add-on 1 - Description", + fullDescription: "Repo Add-on 1 - Full Description & some extra", + get icons() { + return { 32: get_subfile_uri(ADDON_IDS[0], "icon.png") }; + }, + homepageURL: BASE_URL + "/xpi/1/homepage.html", + supportURL: BASE_URL + "/repo/1/support.html", + get optionsURL() { + return extensionURL(ADDON_IDS[0], "options.html"); + }, + averageRating: 1, + reviewCount: 1111, + reviewURL: BASE_URL + "/repo/1/review.html", + weeklyDownloads: 3331, + sourceURI: NetUtil.newURI(ADDON_FILES[0]).spec, + }, + { + id: ADDON_IDS[1], + type: "theme", + name: "XPI Add-on 2", + version: "1.2", + developers: [ + { + name: "Repo Add-on 2 - First Developer", + url: BASE_URL + "/repo/2/firstDeveloper.html", + }, + { + name: "Repo Add-on 2 - Second Developer", + url: BASE_URL + "/repo/2/secondDeveloper.html", + }, + ], + description: "Repo Add-on 2 - Description", + fullDescription: "Repo Add-on 2 - Full Description", + icons: { 32: BASE_URL + "/repo/2/icon.png" }, + screenshots: [ + { + url: BASE_URL + "/repo/2/firstFull.png", + thumbnailURL: BASE_URL + "/repo/2/firstThumbnail.png", + caption: "Repo Add-on 2 - First Caption", + }, + { + url: BASE_URL + "/repo/2/secondFull.png", + thumbnailURL: BASE_URL + "/repo/2/secondThumbnail.png", + caption: "Repo Add-on 2 - Second Caption", + }, + ], + homepageURL: BASE_URL + "/repo/2/homepage.html", + supportURL: BASE_URL + "/repo/2/support.html", + averageRating: 2, + reviewCount: 1112, + reviewURL: BASE_URL + "/repo/2/review.html", + weeklyDownloads: 3332, + sourceURI: NetUtil.newURI(ADDON_FILES[1]).spec, + }, + { + id: ADDON_IDS[2], + type: "theme", + name: "XPI Add-on 3", + version: "1.3", + get iconURL() { + return get_subfile_uri(ADDON_IDS[2], "icon.png"); + }, + get icons() { + return { 32: get_subfile_uri(ADDON_IDS[2], "icon.png") }; + }, + screenshots: [ + { + url: BASE_URL + "/repo/3/firstFull.png", + thumbnailURL: BASE_URL + "/repo/3/firstThumbnail.png", + caption: "Repo Add-on 3 - First Caption", + }, + { + url: BASE_URL + "/repo/3/secondFull.png", + thumbnailURL: BASE_URL + "/repo/3/secondThumbnail.png", + caption: "Repo Add-on 3 - Second Caption", + }, + ], + sourceURI: NetUtil.newURI(ADDON_FILES[2]).spec, + }, +]; + +// Expected add-ons when using cache for extension, but no cache for themes. +const WITH_EXTENSION_CACHE = [ + { + id: ADDON_IDS[0], + type: "extension", + name: "XPI Add-on 1", + version: "1.1", + developers: [ + { + name: "Repo Add-on 1 - First Developer", + url: BASE_URL + "/repo/1/firstDeveloper.html", + }, + { + name: "Repo Add-on 1 - Second Developer", + url: BASE_URL + "/repo/1/secondDeveloper.html", + }, + ], + description: "XPI Add-on 1 - Description", + fullDescription: "Repo Add-on 1 - Full Description & some extra", + get icons() { + return { 32: get_subfile_uri(ADDON_IDS[0], "icon.png") }; + }, + homepageURL: BASE_URL + "/xpi/1/homepage.html", + supportURL: BASE_URL + "/repo/1/support.html", + get optionsURL() { + return extensionURL(ADDON_IDS[0], "options.html"); + }, + averageRating: 1, + reviewCount: 1111, + reviewURL: BASE_URL + "/repo/1/review.html", + weeklyDownloads: 3331, + sourceURI: NetUtil.newURI(ADDON_FILES[0]).spec, + }, + { + id: ADDON_IDS[1], + type: "theme", + name: "XPI Add-on 2", + version: "1.2", + sourceURI: NetUtil.newURI(ADDON_FILES[1]).spec, + icons: {}, + }, + { + id: ADDON_IDS[2], + type: "theme", + name: "XPI Add-on 3", + version: "1.3", + get iconURL() { + return get_subfile_uri(ADDON_IDS[2], "icon.png"); + }, + get icons() { + return { 32: get_subfile_uri(ADDON_IDS[2], "icon.png") }; + }, + screenshots: [ + { + get url() { + return get_subfile_uri(ADDON_IDS[2], "preview.png"); + }, + }, + ], + sourceURI: NetUtil.newURI(ADDON_FILES[2]).spec, + }, +]; + +var gDBFile = gProfD.clone(); +gDBFile.append(FILE_DATABASE); + +/* + * Check the actual add-on results against the expected add-on results + * + * @param aActualAddons + * The array of actual add-ons to check + * @param aExpectedAddons + * The array of expected add-ons to check against + * @param aFromRepository + * An optional boolean representing if the add-ons are from + * the repository + */ +function check_results(aActualAddons, aExpectedAddons, aFromRepository) { + aFromRepository = !!aFromRepository; + + do_check_addons(aActualAddons, aExpectedAddons, ADDON_PROPERTIES); + + // Separately test updateDate (it should only be equal to the + // REPOSITORY values if it is from the repository) + aActualAddons.forEach(function (aActualAddon) { + if (aActualAddon.updateDate) { + let time = aActualAddon.updateDate.getTime(); + Assert.equal(time === 1000 * REPOSITORY_UPDATEDATE, aFromRepository); + } + }); +} + +/* + * Check the add-ons in the cache. This function also tests + * AddonRepository.getCachedAddonByID() + * + * @param aExpectedToFind + * An array of booleans representing which REPOSITORY_ADDONS are + * expected to be found in the cache + * @param aExpectedImmediately + * A boolean representing if results from the cache are expected + * immediately. Results are not immediate if the cache has not been + * initialized yet. + * @return Promise{null} + * Resolves once the checks are complete + */ +function check_cache(aExpectedToFind, aExpectedImmediately) { + Assert.equal(aExpectedToFind.length, REPOSITORY_ADDONS.length); + + let lookups = []; + + for (let i = 0; i < REPOSITORY_ADDONS.length; i++) { + lookups.push( + new Promise((resolve, reject) => { + let immediatelyFound = true; + let expected = aExpectedToFind[i] ? REPOSITORY_ADDONS[i] : null; + // can't Promise-wrap this because we're also testing whether the callback is + // sync or async + AddonRepository.getCachedAddonByID( + REPOSITORY_ADDONS[i].id, + function (aAddon) { + Assert.equal(immediatelyFound, aExpectedImmediately); + if (expected == null) { + Assert.equal(aAddon, null); + } else { + check_results([aAddon], [expected], true); + } + resolve(); + } + ); + immediatelyFound = false; + }) + ); + } + return Promise.all(lookups); +} + +/* + * Task to check an initialized cache by checking the cache, then restarting the + * manager, and checking the cache. This checks that the cache is consistent + * across manager restarts. + * + * @param aExpectedToFind + * An array of booleans representing which REPOSITORY_ADDONS are + * expected to be found in the cache + */ +async function check_initialized_cache(aExpectedToFind) { + await check_cache(aExpectedToFind, true); + await promiseRestartManager(); + + // If cache is disabled, then expect results immediately + let cacheEnabled = Services.prefs.getBoolPref(PREF_GETADDONS_CACHE_ENABLED); + await check_cache(aExpectedToFind, !cacheEnabled); +} + +add_task(async function setup() { + // Setup for test + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9"); + + await promiseStartupManager(); + + // Install XPI add-ons + await promiseInstallAllFiles(ADDON_FILES); + await promiseRestartManager(); + + gServer = AddonTestUtils.createHttpServer({ hosts: [HOST] }); + gServer.registerDirectory("/data/", do_get_file("data")); +}); + +// Tests AddonRepository.cacheEnabled +add_task(async function run_test_1() { + Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false); + Assert.ok(!AddonRepository.cacheEnabled); + Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true); + Assert.ok(AddonRepository.cacheEnabled); +}); + +// Tests that the cache and database begin as empty +add_task(async function run_test_2() { + Assert.ok(!gDBFile.exists()); + await check_cache([false, false, false], false); + await AddonRepository.flush(); +}); + +// Tests repopulateCache when the search fails +add_task(async function run_test_3() { + Assert.ok(gDBFile.exists()); + Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true); + Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, FAILED_RESULT); + + await AddonRepository.backgroundUpdateCheck(); + await check_initialized_cache([false, false, false]); +}); + +// Tests repopulateCache when search returns no results +add_task(async function run_test_4() { + Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, EMPTY_RESULT); + + await AddonRepository.backgroundUpdateCheck(); + await check_initialized_cache([false, false, false]); +}); + +// Tests repopulateCache when search returns results +add_task(async function run_test_5() { + Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_RESULTS); + + await AddonRepository.backgroundUpdateCheck(); + await check_initialized_cache([true, true, true]); +}); + +// Tests repopulateCache when caching is disabled for a single add-on +add_task(async function run_test_5_1() { + Services.prefs.setBoolPref(PREF_ADDON0_CACHE_ENABLED, false); + + await AddonRepository.backgroundUpdateCheck(); + + // Reset pref for next test + Services.prefs.setBoolPref(PREF_ADDON0_CACHE_ENABLED, true); + + await check_initialized_cache([false, true, true]); +}); + +// Tests repopulateCache when caching is disabled +add_task(async function run_test_6() { + Assert.ok(gDBFile.exists()); + Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false); + + await AddonRepository.backgroundUpdateCheck(); + // Database should have been deleted + Assert.ok(!gDBFile.exists()); + + Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true); + await check_cache([false, false, false], false); + await AddonRepository.flush(); +}); + +// Tests cacheAddons when the search fails +add_task(async function run_test_7() { + Assert.ok(gDBFile.exists()); + Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, FAILED_RESULT); + + await AddonRepository.cacheAddons(ADDON_IDS); + await check_initialized_cache([false, false, false]); +}); + +// Tests cacheAddons when the search returns no results +add_task(async function run_test_8() { + Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, EMPTY_RESULT); + + await AddonRepository.cacheAddons(ADDON_IDS); + await check_initialized_cache([false, false, false]); +}); + +// Tests cacheAddons for a single add-on when search returns results +add_task(async function run_test_9() { + Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_RESULTS); + + await AddonRepository.cacheAddons([ADDON_IDS[0]]); + await check_initialized_cache([true, false, false]); +}); + +// Tests cacheAddons when caching is disabled for a single add-on +add_task(async function run_test_9_1() { + Services.prefs.setBoolPref(PREF_ADDON1_CACHE_ENABLED, false); + + await AddonRepository.cacheAddons(ADDON_IDS); + + // Reset pref for next test + Services.prefs.setBoolPref(PREF_ADDON1_CACHE_ENABLED, true); + + await check_initialized_cache([true, false, true]); +}); + +// Tests cacheAddons for multiple add-ons, some already in the cache, +add_task(async function run_test_10() { + await AddonRepository.cacheAddons(ADDON_IDS); + await check_initialized_cache([true, true, true]); +}); + +// Tests cacheAddons when caching is disabled +add_task(async function run_test_11() { + Assert.ok(gDBFile.exists()); + Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false); + + await AddonRepository.cacheAddons(ADDON_IDS); + Assert.ok(gDBFile.exists()); + + Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true); + await check_initialized_cache([true, true, true]); +}); + +// Tests that XPI add-ons do not use any of the repository properties if +// caching is disabled, even if there are repository properties available +add_task(async function run_test_12() { + Assert.ok(gDBFile.exists()); + Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false); + Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_RESULTS); + + let addons = await promiseAddonsByIDs(ADDON_IDS); + check_results(addons, WITHOUT_CACHE); +}); + +// Tests that a background update with caching disabled deletes the add-ons +// database, and that XPI add-ons still do not use any of repository properties +add_task(async function run_test_13() { + Assert.ok(gDBFile.exists()); + Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, EMPTY_RESULT); + + await AddonManagerPrivate.backgroundUpdateCheck(); + // Database should have been deleted + Assert.ok(!gDBFile.exists()); + + let aAddons = await promiseAddonsByIDs(ADDON_IDS); + check_results(aAddons, WITHOUT_CACHE); +}); + +// Tests that the XPI add-ons have the correct properties if caching is +// enabled but has no information +add_task(async function run_test_14() { + Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true); + + await AddonManagerPrivate.backgroundUpdateCheck(); + await AddonRepository.flush(); + Assert.ok(gDBFile.exists()); + + let aAddons = await promiseAddonsByIDs(ADDON_IDS); + check_results(aAddons, WITHOUT_CACHE); +}); + +// Tests that the XPI add-ons correctly use the repository properties when +// caching is enabled and the repository information is available +add_task(async function run_test_15() { + Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_RESULTS); + + await AddonManagerPrivate.backgroundUpdateCheck(); + let aAddons = await promiseAddonsByIDs(ADDON_IDS); + check_results(aAddons, WITH_CACHE); +}); + +// Tests that restarting the manager does not change the checked properties +// on the XPI add-ons (repository properties still exist and are still properly +// used) +add_task(async function run_test_16() { + await promiseRestartManager(); + + let aAddons = await promiseAddonsByIDs(ADDON_IDS); + check_results(aAddons, WITH_CACHE); +}); + +// Tests that setting a list of types to cache works +add_task(async function run_test_17() { + Services.prefs.setCharPref( + PREF_GETADDONS_CACHE_TYPES, + "foo,bar,extension,baz" + ); + + await AddonManagerPrivate.backgroundUpdateCheck(); + let aAddons = await promiseAddonsByIDs(ADDON_IDS); + check_results(aAddons, WITH_EXTENSION_CACHE); + Services.prefs.clearUserPref(PREF_GETADDONS_CACHE_TYPES); +}); + +// Tests that the cache is retained when the server/API is unreachable. +// Regression test for https://bugzilla.mozilla.org/show_bug.cgi?id=1870905 +add_task(async function run_test_18() { + // The response is expected to be JSON, so setting it to non-JSON is + // equivalent to the server being unreachable in the implementation. + Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, "data:text/not-json,"); + + await AddonManagerPrivate.backgroundUpdateCheck(); + let aAddons = await promiseAddonsByIDs(ADDON_IDS); + check_results(aAddons, WITH_EXTENSION_CACHE); + Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_RESULTS); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache_locale.js b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache_locale.js new file mode 100644 index 0000000000..37e60e27dd --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache_locale.js @@ -0,0 +1,217 @@ +"user strict"; + +const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled"; +const PREF_METADATA_LASTUPDATE = "extensions.getAddons.cache.lastUpdate"; +Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +let server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); +server.registerDirectory("/data/", do_get_file("data")); + +// Use %LOCALE% as the default pref does. It is set from appLocaleAsBCP47. +Services.prefs.setStringPref( + PREF_GETADDONS_BYIDS, + "http://example.com/addons.json?guids=%IDS%&locale=%LOCALE%" +); + +const TEST_ADDON_ID = "test_AddonRepository_1@tests.mozilla.org"; + +const repositoryAddons = { + "test_AddonRepository_1@tests.mozilla.org": { + name: "Repo Add-on 1", + type: "extension", + guid: TEST_ADDON_ID, + current_version: { + version: "2.1", + files: [ + { + platform: "all", + size: 9, + url: "http://example.com/repo/1/install.xpi", + }, + ], + }, + }, + "langpack-und@test.mozilla.org": { + // included only to avoid exceptions in AddonRepository + name: "und langpack", + type: "language", + guid: "langpack-und@test.mozilla.org", + current_version: { + version: "1.1", + files: [ + { + platform: "all", + size: 9, + url: "http://example.com/repo/1/langpack.xpi", + }, + ], + }, + }, +}; + +server.registerPathHandler("/addons.json", (request, response) => { + let search = new URLSearchParams(request.queryString); + let IDs = search.get("guids").split(","); + let locale = search.get("locale"); + + let repositoryData = { + page_size: 25, + page_count: 1, + count: 0, + next: null, + previous: null, + results: [], + }; + for (let id of IDs) { + let data = JSON.parse(JSON.stringify(repositoryAddons[id])); + data.summary = `This is an ${locale} addon data object`; + data.description = `Full Description ${locale}`; + repositoryData.results.push(data); + } + repositoryData.count = repositoryData.results.length; + + // The request contains the IDs to retreive, but we're just handling the + // two test addons so it's static data. + response.setHeader("content-type", "application/json"); + response.write(JSON.stringify(repositoryData)); +}); + +const ADDONS = [ + { + manifest: { + name: "XPI Add-on 1", + version: "1.1", + + description: "XPI Add-on 1 - Description", + developer: { + name: "XPI Add-on 1 - Author", + }, + + homepage_url: "http://example.com/xpi/1/homepage.html", + icons: { + 32: "icon.png", + }, + + options_ui: { + page: "options.html", + }, + + browser_specific_settings: { + gecko: { id: TEST_ADDON_ID }, + }, + }, + }, + { + // Necessary to provide the "und" locale + manifest: { + name: "und Language Pack", + version: "1.0", + manifest_version: 2, + browser_specific_settings: { + gecko: { + id: "langpack-und@test.mozilla.org", + }, + }, + sources: { + browser: { + base_path: "browser/", + }, + }, + langpack_id: "und", + languages: { + und: { + chrome_resources: { + global: "chrome/und/locale/und/global/", + }, + version: "20171001190118", + }, + }, + author: "Mozilla Localization Task Force", + description: "Language pack for Testy for und", + }, + }, +]; +const ADDON_FILES = ADDONS.map(addon => + AddonTestUtils.createTempWebExtensionFile(addon) +); + +const REQ_LOC_CHANGE_EVENT = "intl:requested-locales-changed"; + +function promiseLocaleChanged(requestedLocale) { + if (Services.locale.appLocaleAsBCP47 == requestedLocale) { + return Promise.resolve(); + } + return new Promise(resolve => { + let localeObserver = { + observe(aSubject, aTopic, aData) { + switch (aTopic) { + case REQ_LOC_CHANGE_EVENT: + let reqLocs = Services.locale.requestedLocales; + equal(reqLocs[0], requestedLocale); + Services.obs.removeObserver(localeObserver, REQ_LOC_CHANGE_EVENT); + resolve(); + } + }, + }; + Services.obs.addObserver(localeObserver, REQ_LOC_CHANGE_EVENT); + Services.locale.requestedLocales = [requestedLocale]; + }); +} + +function promiseMetaDataUpdate() { + return new Promise(resolve => { + let listener = args => { + Services.prefs.removeObserver(PREF_METADATA_LASTUPDATE, listener); + resolve(); + }; + + Services.prefs.addObserver(PREF_METADATA_LASTUPDATE, listener); + }); +} + +function promiseLocale(locale) { + return Promise.all([promiseLocaleChanged(locale), promiseMetaDataUpdate()]); +} + +add_task(async function setup() { + await promiseStartupManager(); + for (let xpi of ADDON_FILES) { + await promiseInstallFile(xpi); + } +}); + +add_task(async function test_locale_change() { + await promiseLocale("en-US"); + let addon = await AddonRepository.getCachedAddonByID(TEST_ADDON_ID); + Assert.ok(addon.description.includes("en-US"), "description is en-us"); + Assert.ok( + addon.fullDescription.includes("en-US"), + "fullDescription is en-us" + ); + + // This pref is a 1s resolution, set it to zero so the + // next test can wait on it being updated again. + Services.prefs.setIntPref(PREF_METADATA_LASTUPDATE, 0); + // Wait for the last update timestamp to be updated. + await promiseLocale("und"); + + addon = await AddonRepository.getCachedAddonByID(TEST_ADDON_ID); + + Assert.ok( + addon.description.includes("und"), + `description is ${addon.description}` + ); + Assert.ok( + addon.fullDescription.includes("und"), + `fullDescription is ${addon.fullDescription}` + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_langpacks.js b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_langpacks.js new file mode 100644 index 0000000000..e84ce4ea30 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_langpacks.js @@ -0,0 +1,135 @@ +const PREF_GET_LANGPACKS = "extensions.getAddons.langpacks.url"; + +let server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); +Services.prefs.setStringPref( + PREF_GET_LANGPACKS, + "http://example.com/langpacks.json" +); + +add_task(async function test_getlangpacks() { + function setData(data) { + if (typeof data != "string") { + data = JSON.stringify(data); + } + + server.registerPathHandler("/langpacks.json", (request, response) => { + response.setHeader("content-type", "application/json"); + response.write(data); + }); + } + + const EXPECTED = [ + { + target_locale: "kl", + url: "http://example.com/langpack1.xpi", + hash: "sha256:0123456789abcdef", + }, + { + target_locale: "fo", + url: "http://example.com/langpack2.xpi", + hash: "sha256:fedcba9876543210", + }, + ]; + + setData({ + results: [ + // A simple entry + { + target_locale: EXPECTED[0].target_locale, + current_compatible_version: { + files: [ + { + platform: "all", + url: EXPECTED[0].url, + hash: EXPECTED[0].hash, + }, + ], + }, + }, + + // An entry with multiple supported platforms + { + target_locale: EXPECTED[1].target_locale, + current_compatible_version: { + files: [ + { + platform: "somethingelse", + url: "http://example.com/bogus.xpi", + hash: "sha256:abcd", + }, + { + platform: Services.appinfo.OS.toLowerCase(), + url: EXPECTED[1].url, + hash: EXPECTED[1].hash, + }, + ], + }, + }, + + // An entry with no matching platform + { + target_locale: "bla", + current_compatible_version: { + files: [ + { + platform: "unsupportedplatform", + url: "http://example.com/bogus2.xpi", + hash: "sha256:1234", + }, + ], + }, + }, + ], + }); + + let result = await AddonRepository.getAvailableLangpacks(); + equal(result.length, 2, "Got 2 results"); + + deepEqual(result[0], EXPECTED[0], "Got expected result for simple entry"); + deepEqual( + result[1], + EXPECTED[1], + "Got expected result for multi-platform entry" + ); + + setData("not valid json"); + await Assert.rejects( + AddonRepository.getAvailableLangpacks(), + /SyntaxError/, + "Got parse error on invalid JSON" + ); +}); + +// Tests that cookies are not sent with langpack requests. +add_task(async function test_cookies() { + let lastRequest = null; + server.registerPathHandler("/langpacks.json", (request, response) => { + lastRequest = request; + response.write(JSON.stringify({ results: [] })); + }); + + const COOKIE = "test"; + let expiration = Date.now() / 1000 + 60 * 60; + Services.cookies.add( + "example.com", + "/", + COOKIE, + "testing", + false, + false, + false, + expiration, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTP + ); + + await AddonRepository.getAvailableLangpacks(); + + notEqual(lastRequest, null, "Received langpack request"); + equal( + lastRequest.hasHeader("Cookie"), + false, + "Langpack request has no cookies" + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_paging.js b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_paging.js new file mode 100644 index 0000000000..0773917d84 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_paging.js @@ -0,0 +1,91 @@ +// Test that AMO api results that are returned in muliple pages are +// properly handled. +add_task(async function test_paged_api() { + const MAX_ADDON = 3; + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "2"); + + let testserver = createHttpServer(); + const PORT = testserver.identity.primaryPort; + + const EMPTY_RESPONSE = { + next: null, + results: [], + }; + + function name(n) { + return `Addon ${n}`; + } + function id(n) { + return `test${n}@tests.mozilla.org`; + } + function summary(n) { + return `Summary for addon ${n}`; + } + function description(n) { + return `Description for addon ${n}`; + } + + testserver.registerPathHandler("/empty", (request, response) => { + response.setHeader("content-type", "application/json"); + response.write(JSON.stringify(EMPTY_RESPONSE)); + }); + + testserver.registerPrefixHandler("/addons/", (request, response) => { + let [page] = /\d+/.exec(request.path); + page = page ? parseInt(page, 10) : 0; + page = Math.min(page, MAX_ADDON); + + let result = { + next: + page == MAX_ADDON + ? null + : `http://localhost:${PORT}/addons/${page + 1}`, + results: [ + { + name: name(page), + type: "extension", + guid: id(page), + summary: summary(page), + description: description(page), + }, + ], + }; + + response.setHeader("content-type", "application/json"); + response.write(JSON.stringify(result)); + }); + + Services.prefs.setCharPref( + PREF_GETADDONS_BYIDS, + `http://localhost:${PORT}/addons/0` + ); + + await promiseStartupManager(); + + for (let i = 0; i <= MAX_ADDON; i++) { + await promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { gecko: { id: id(i) } }, + }, + }); + } + + await AddonManagerPrivate.backgroundUpdateCheck(); + + let ids = []; + for (let i = 0; i <= MAX_ADDON; i++) { + ids.push(id(i)); + } + let addons = await AddonRepository.getAddonsByIDs(ids); + + equal(addons.length, MAX_ADDON + 1); + for (let i = 0; i <= MAX_ADDON; i++) { + equal(addons[i].name, name(i)); + equal(addons[i].id, id(i)); + equal(addons[i].description, summary(i)); + equal(addons[i].fullDescription, description(i)); + } + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_ProductAddonChecker.js b/toolkit/mozapps/extensions/test/xpcshell/test_ProductAddonChecker.js new file mode 100644 index 0000000000..04373e85ff --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_ProductAddonChecker.js @@ -0,0 +1,310 @@ +"use strict"; + +const { ProductAddonChecker } = ChromeUtils.importESModule( + "resource://gre/modules/addons/ProductAddonChecker.sys.mjs" +); + +const LocalFile = new Components.Constructor( + "@mozilla.org/file/local;1", + Ci.nsIFile, + "initWithPath" +); + +Services.prefs.setBoolPref("media.gmp-manager.updateEnabled", true); + +var testserver = new HttpServer(); +testserver.registerDirectory("/data/", do_get_file("data/productaddons")); +testserver.start(); +var root = + testserver.identity.primaryScheme + + "://" + + testserver.identity.primaryHost + + ":" + + testserver.identity.primaryPort + + "/data/"; + +/** + * Compares binary data of 2 arrays and returns true if they are the same + * + * @param arr1 The first array to compare + * @param arr2 The second array to compare + */ +function compareBinaryData(arr1, arr2) { + Assert.equal(arr1.length, arr2.length); + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] != arr2[i]) { + info( + "Data differs at index " + + i + + ", arr1: " + + arr1[i] + + ", arr2: " + + arr2[i] + ); + return false; + } + } + return true; +} + +/** + * Reads a file's data and returns it + * + * @param file The file to read the data from + * @return array of bytes for the data in the file. + */ +function getBinaryFileData(file) { + let fileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + // Open as RD_ONLY with default permissions. + fileStream.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); + + let stream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + stream.setInputStream(fileStream); + let bytes = stream.readByteArray(stream.available()); + fileStream.close(); + return bytes; +} + +/** + * Compares binary data of 2 files and returns true if they are the same + * + * @param file1 The first file to compare + * @param file2 The second file to compare + */ +function compareFiles(file1, file2) { + return compareBinaryData(getBinaryFileData(file1), getBinaryFileData(file2)); +} + +add_task(async function test_404() { + await Assert.rejects( + ProductAddonChecker.getProductAddonList(root + "404.xml"), + /got node name: html/ + ); +}); + +add_task(async function test_not_xml() { + await Assert.rejects( + ProductAddonChecker.getProductAddonList(root + "bad.txt"), + /got node name: parsererror/ + ); +}); + +add_task(async function test_invalid_xml() { + await Assert.rejects( + ProductAddonChecker.getProductAddonList(root + "bad.xml"), + /got node name: parsererror/ + ); +}); + +add_task(async function test_wrong_xml() { + await Assert.rejects( + ProductAddonChecker.getProductAddonList(root + "bad2.xml"), + /got node name: test/ + ); +}); + +add_task(async function test_missing() { + let addons = await ProductAddonChecker.getProductAddonList( + root + "missing.xml" + ); + Assert.equal(addons, null); +}); + +add_task(async function test_empty() { + let res = await ProductAddonChecker.getProductAddonList(root + "empty.xml"); + Assert.ok(Array.isArray(res.addons)); + Assert.equal(res.addons.length, 0); +}); + +add_task(async function test_good_xml() { + let res = await ProductAddonChecker.getProductAddonList(root + "good.xml"); + Assert.ok(Array.isArray(res.addons)); + + // There are three valid entries in the XML + Assert.equal(res.addons.length, 5); + + let addon = res.addons[0]; + Assert.equal(addon.id, "test1"); + Assert.equal(addon.URL, "http://example.com/test1.xpi"); + Assert.equal(addon.hashFunction, undefined); + Assert.equal(addon.hashValue, undefined); + Assert.equal(addon.version, undefined); + Assert.equal(addon.size, undefined); + + addon = res.addons[1]; + Assert.equal(addon.id, "test2"); + Assert.equal(addon.URL, "http://example.com/test2.xpi"); + Assert.equal(addon.hashFunction, "md5"); + Assert.equal(addon.hashValue, "djhfgsjdhf"); + Assert.equal(addon.version, undefined); + Assert.equal(addon.size, undefined); + + addon = res.addons[2]; + Assert.equal(addon.id, "test3"); + Assert.equal(addon.URL, "http://example.com/test3.xpi"); + Assert.equal(addon.hashFunction, undefined); + Assert.equal(addon.hashValue, undefined); + Assert.equal(addon.version, "1.0"); + Assert.equal(addon.size, 45); + + addon = res.addons[3]; + Assert.equal(addon.id, "test4"); + Assert.equal(addon.URL, undefined); + Assert.equal(addon.hashFunction, undefined); + Assert.equal(addon.hashValue, undefined); + Assert.equal(addon.version, undefined); + Assert.equal(addon.size, undefined); + + addon = res.addons[4]; + Assert.equal(addon.id, undefined); + Assert.equal(addon.URL, "http://example.com/test5.xpi"); + Assert.equal(addon.hashFunction, undefined); + Assert.equal(addon.hashValue, undefined); + Assert.equal(addon.version, undefined); + Assert.equal(addon.size, undefined); +}); + +add_task(async function test_download_nourl() { + try { + let path = await ProductAddonChecker.downloadAddon({}); + + await IOUtils.remove(path); + do_throw("Should not have downloaded a file with a missing url"); + } catch (e) { + Assert.ok( + true, + "Should have thrown when downloading a file with a missing url." + ); + } +}); + +add_task(async function test_download_missing() { + try { + let path = await ProductAddonChecker.downloadAddon({ + URL: root + "nofile.xpi", + }); + + await IOUtils.remove(path); + do_throw("Should not have downloaded a missing file"); + } catch (e) { + Assert.ok(true, "Should have thrown when downloading a missing file."); + } +}); + +add_task(async function test_download_noverify() { + let path = await ProductAddonChecker.downloadAddon({ + URL: root + "unsigned.xpi", + }); + + let stat = await IOUtils.stat(path); + Assert.ok(!stat.type !== "directory"); + Assert.equal(stat.size, 452); + + Assert.ok( + compareFiles( + do_get_file("data/productaddons/unsigned.xpi"), + new LocalFile(path) + ) + ); + + await IOUtils.remove(path); +}); + +add_task(async function test_download_badsize() { + try { + let path = await ProductAddonChecker.downloadAddon({ + URL: root + "unsigned.xpi", + size: 400, + }); + + await IOUtils.remove(path); + do_throw("Should not have downloaded a file with a bad size"); + } catch (e) { + Assert.ok( + true, + "Should have thrown when downloading a file with a bad size." + ); + } +}); + +add_task(async function test_download_badhashfn() { + try { + let path = await ProductAddonChecker.downloadAddon({ + URL: root + "unsigned.xpi", + hashFunction: "sha2567", + hashValue: + "9b9abf7ddfc1a6d7ffc7e0247481dcc202363e4445ad3494fb22036f1698c7f3", + }); + + await IOUtils.remove(path); + do_throw("Should not have downloaded a file with a bad hash function"); + } catch (e) { + Assert.ok( + true, + "Should have thrown when downloading a file with a bad hash function." + ); + } +}); + +add_task(async function test_download_sha1_unsupported() { + try { + let path = await ProductAddonChecker.downloadAddon({ + URL: root + "unsigned.xpi", + hashFunction: "sha1", + hashValue: "3d0dc22e1f394e159b08aaf5f0f97de4d5c65f4f", + }); + + await IOUtils.remove(path); + do_throw("Should not have downloaded a file with a bad hash function"); + } catch (e) { + Assert.ok( + true, + "Should have thrown when downloading a file with a bad hash function." + ); + } +}); + +add_task(async function test_download_badhash() { + try { + let path = await ProductAddonChecker.downloadAddon({ + URL: root + "unsigned.xpi", + hashFunction: "sha256", + hashValue: + "8b9abf7ddfc1a6d7ffc7e0247481dcc202363e4445ad3494fb22036f1698c7f3", + }); + + await IOUtils.remove(path); + do_throw("Should not have downloaded a file with a bad hash"); + } catch (e) { + Assert.ok( + true, + "Should have thrown when downloading a file with a bad hash." + ); + } +}); + +add_task(async function test_download_works() { + let path = await ProductAddonChecker.downloadAddon({ + URL: root + "unsigned.xpi", + size: 452, + hashFunction: "sha256", + hashValue: + "9b9abf7ddfc1a6d7ffc7e0247481dcc202363e4445ad3494fb22036f1698c7f3", + }); + + let stat = await IOUtils.stat(path); + Assert.ok(stat.type !== "directory"); + + Assert.ok( + compareFiles( + do_get_file("data/productaddons/unsigned.xpi"), + new LocalFile(path) + ) + ); + + await IOUtils.remove(path); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_ProductAddonChecker_signatures.js b/toolkit/mozapps/extensions/test/xpcshell/test_ProductAddonChecker_signatures.js new file mode 100644 index 0000000000..5ae61568ef --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_ProductAddonChecker_signatures.js @@ -0,0 +1,201 @@ +"use strict"; + +const { ProductAddonChecker } = ChromeUtils.importESModule( + "resource://gre/modules/addons/ProductAddonChecker.sys.mjs" +); + +Services.prefs.setBoolPref("media.gmp-manager.updateEnabled", true); + +// Setup a test server for content signature tests. +const signedTestServer = new HttpServer(); +const testDataDir = "data/productaddons/"; + +// Start the server so we can grab the identity. We need to know this so the +// server can reference itself in the handlers that will be set up. +signedTestServer.start(); +const signedBaseUri = + signedTestServer.identity.primaryScheme + + "://" + + signedTestServer.identity.primaryHost + + ":" + + signedTestServer.identity.primaryPort; + +// Setup endpoint to handle x5u lookups correctly. +const validX5uPath = "/valid_x5u"; +// These certificates are generated using ./mach generate-test-certs <path_to_certspec> +const validCertChain = loadCertChain(testDataDir + "content_signing", [ + "aus_ee", + "int", +]); +signedTestServer.registerPathHandler(validX5uPath, (req, res) => { + res.write(validCertChain.join("\n")); +}); +const validX5uUrl = signedBaseUri + validX5uPath; + +// Setup endpoint to handle x5u lookups incorrectly. +const invalidX5uPath = "/invalid_x5u"; +const invalidCertChain = loadCertChain(testDataDir + "content_signing", [ + "aus_ee", + // This cert chain is missing the intermediate cert! +]); +signedTestServer.registerPathHandler(invalidX5uPath, (req, res) => { + res.write(invalidCertChain.join("\n")); +}); +const invalidX5uUrl = signedBaseUri + invalidX5uPath; + +// Will hold the XML data from good.xml. +let goodXml; +// This sig is generated using the following command at mozilla-central root +// `cat toolkit/mozapps/extensions/test/xpcshell/data/productaddons/good.xml | ./mach python security/manager/ssl/tests/unit/test_content_signing/pysign.py` +// If test certificates are regenerated, this signature must also be. +const goodXmlContentSignature = + "7QYnPqFoOlS02BpDdIRIljzmPr6BFwPs1z1y8KJUBlnU7EVG6FbnXmVVt5Op9wDzgvhXX7th8qFJvpPOZs_B_tHRDNJ8SK0HN95BAN15z3ZW2r95SSHmU-fP2JgoNOR3"; + +const goodXmlPath = "/good.xml"; +// Requests use query strings to test different signature states. +const validSignatureQuery = "validSignature"; +const invalidSignatureQuery = "invalidSignature"; +const missingSignatureQuery = "missingSignature"; +const incompleteSignatureQuery = "incompleteSignature"; +const badX5uSignatureQuery = "badX5uSignature"; +signedTestServer.registerPathHandler(goodXmlPath, (req, res) => { + if (req.queryString == validSignatureQuery) { + res.setHeader( + "content-signature", + `x5u=${validX5uUrl}; p384ecdsa=${goodXmlContentSignature}` + ); + } else if (req.queryString == invalidSignatureQuery) { + res.setHeader("content-signature", `x5u=${validX5uUrl}; p384ecdsa=garbage`); + } else if (req.queryString == missingSignatureQuery) { + // Intentionally don't set the header. + } else if (req.queryString == incompleteSignatureQuery) { + res.setHeader( + "content-signature", + `x5u=${validX5uUrl}` // There's no p384ecdsa part! + ); + } else if (req.queryString == badX5uSignatureQuery) { + res.setHeader( + "content-signature", + `x5u=${invalidX5uUrl}; p384ecdsa=${goodXmlContentSignature}` + ); + } else { + Assert.ok( + false, + "Invalid queryString passed to server! Tests shouldn't do that!" + ); + } + res.write(goodXml); +}); + +// Handle aysnc load of good.xml. +add_task(async function load_good_xml() { + goodXml = await IOUtils.readUTF8(do_get_file(testDataDir + "good.xml").path); +}); + +add_task(async function test_valid_content_signature() { + try { + const res = await ProductAddonChecker.getProductAddonList( + signedBaseUri + goodXmlPath + "?" + validSignatureQuery, + /*allowNonBuiltIn*/ false, + /*allowedCerts*/ false, + /*verifyContentSignature*/ true + ); + Assert.ok(true, "Should successfully get addon list"); + + // Smoke test the results are as expected. + Assert.equal(res.addons[0].id, "test1"); + Assert.equal(res.addons[1].id, "test2"); + Assert.equal(res.addons[2].id, "test3"); + Assert.equal(res.addons[3].id, "test4"); + Assert.equal(res.addons[4].id, undefined); + } catch (e) { + Assert.ok( + false, + `Should successfully get addon list, instead failed with ${e}` + ); + } +}); + +add_task(async function test_invalid_content_signature() { + try { + await ProductAddonChecker.getProductAddonList( + signedBaseUri + goodXmlPath + "?" + invalidSignatureQuery, + /*allowNonBuiltIn*/ false, + /*allowedCerts*/ false, + /*verifyContentSignature*/ true + ); + Assert.ok(false, "Should fail to get addon list"); + } catch (e) { + Assert.ok(true, "Should fail to get addon list"); + // The nsIContentSignatureVerifier will throw an error on this path, + // check that we've caught and re-thrown, but don't check the full error + // message as it's messy and subject to change. + Assert.ok( + e.message.startsWith("Content signature validation failed:"), + "Should get signature failure message" + ); + } +}); + +add_task(async function test_missing_content_signature_header() { + try { + await ProductAddonChecker.getProductAddonList( + signedBaseUri + goodXmlPath + "?" + missingSignatureQuery, + /*allowNonBuiltIn*/ false, + /*allowedCerts*/ false, + /*verifyContentSignature*/ true + ); + Assert.ok(false, "Should fail to get addon list"); + } catch (e) { + Assert.ok(true, "Should fail to get addon list"); + Assert.equal( + e.addonCheckerErr, + ProductAddonChecker.VERIFICATION_MISSING_DATA_ERR + ); + Assert.equal( + e.message, + "Content signature validation failed: missing content signature header" + ); + } +}); + +add_task(async function test_incomplete_content_signature_header() { + try { + await ProductAddonChecker.getProductAddonList( + signedBaseUri + goodXmlPath + "?" + incompleteSignatureQuery, + /*allowNonBuiltIn*/ false, + /*allowedCerts*/ false, + /*verifyContentSignature*/ true + ); + Assert.ok(false, "Should fail to get addon list"); + } catch (e) { + Assert.ok(true, "Should fail to get addon list"); + Assert.equal( + e.addonCheckerErr, + ProductAddonChecker.VERIFICATION_MISSING_DATA_ERR + ); + Assert.equal( + e.message, + "Content signature validation failed: missing signature" + ); + } +}); + +add_task(async function test_bad_x5u_content_signature_header() { + try { + await ProductAddonChecker.getProductAddonList( + signedBaseUri + goodXmlPath + "?" + badX5uSignatureQuery, + /*allowNonBuiltIn*/ false, + /*allowedCerts*/ false, + /*verifyContentSignature*/ true + ); + Assert.ok(false, "Should fail to get addon list"); + } catch (e) { + Assert.ok(true, "Should fail to get addon list"); + Assert.equal( + e.addonCheckerErr, + ProductAddonChecker.VERIFICATION_INVALID_ERR + ); + Assert.equal(e.message, "Content signature is not valid"); + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_QuarantinedDomains_AMRemoteSettings.js b/toolkit/mozapps/extensions/test/xpcshell/test_QuarantinedDomains_AMRemoteSettings.js new file mode 100644 index 0000000000..9bca9d17b1 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_QuarantinedDomains_AMRemoteSettings.js @@ -0,0 +1,217 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Globals imported from head_telemetry.js +/* globals setupTelemetryForTests, resetTelemetryData */ + +const { QuarantinedDomains } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionPermissions.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + computeSha1HashAsString: "resource://gre/modules/addons/crypto-utils.sys.mjs", +}); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42", "42"); + +const QUARANTINE_LIST_PREF = "extensions.quarantinedDomains.list"; + +function assertQuarantinedListPref(expectedPrefValue) { + Assert.equal( + Services.prefs.getPrefType(QUARANTINE_LIST_PREF), + Services.prefs.PREF_STRING, + `Expect ${QUARANTINE_LIST_PREF} preference type to be string` + ); + + Assert.equal( + Services.prefs.getStringPref(QUARANTINE_LIST_PREF), + expectedPrefValue, + `Got the expected value set on ${QUARANTINE_LIST_PREF}` + ); +} + +function assertQuarantinedListTelemetry(expectedTelemetryHash) { + Assert.deepEqual( + { + listhash: Glean.extensionsQuarantinedDomains.listhash.testGetValue(), + remotehash: Glean.extensionsQuarantinedDomains.remotehash.testGetValue(), + }, + expectedTelemetryHash, + "Got the expected computed domains list probes recorded by the Glean metrics" + ); + + const scalars = Services.telemetry.getSnapshotForScalars().parent; + Assert.deepEqual( + { + listhash: scalars?.["extensions.quarantinedDomains.listhash"], + remotehash: scalars?.["extensions.quarantinedDomains.remotehash"], + }, + expectedTelemetryHash, + "Got the expected metrics mirrored into the unified telemetry scalars" + ); +} + +async function testQuarantinedDomainsFromRemoteSettings() { + // Same as MAX_PREF_LENGTH as defined in Preferences.cpp, + // see https://searchfox.org/mozilla-central/rev/06510249/modules/libpref/Preferences.cpp#162 + const MAX_PREF_LENGTH = 1 * 1024 * 1024; + const quarantinedDomainsSets = { + testSet1: "example.com,example.org", + testSet2: "someothersite.org,testset2.org", + }; + + // Make sure there isn't initially any pre-existing telemetry data. + resetTelemetryData(); + + await setAndEmitFakeRemoteSettingsData([ + { + id: "quarantinedDomains-01-testSet-toolong", + // We expect this entry to throw when trying to set a string pref + // that doesn't fit in the string prefs size limits. + quarantinedDomains: { + [QUARANTINE_LIST_PREF]: "x".repeat(MAX_PREF_LENGTH + 1), + }, + installTriggerDeprecation: null, + }, + { + id: "quarantinedDomains-02-testSet1", + quarantinedDomains: { + [QUARANTINE_LIST_PREF]: quarantinedDomainsSets.testSet1, + }, + installTriggerDeprecation: null, + }, + { + // We expect this pref to override the pref set based on the + // previous entry. + id: "quarantinedDomains-03-testSet2", + quarantinedDomains: { + [QUARANTINE_LIST_PREF]: quarantinedDomainsSets.testSet2, + }, + installTriggerDeprecation: null, + }, + { + // Expect this entry to leave the domains list pref unchanged. + id: "quarantinedDomains-04-null", + quarantinedDomains: null, + installTriggerDeprecation: null, + }, + ]); + + Assert.equal( + Services.prefs.getPrefType(QUARANTINE_LIST_PREF), + Services.prefs.PREF_STRING, + `Expect ${QUARANTINE_LIST_PREF} preference type to be string` + ); + // The entry too big to fix in the pref value should throw but not preventing + // the other entries from being processed. + // The Last collection entry setting the pref wins, and so we expect + // the pref to be set to the domains listed in the collection + // entry with id "quarantinedDomains-testSet2". + assertQuarantinedListPref(quarantinedDomainsSets.testSet2); + assertQuarantinedListTelemetry({ + listhash: computeSha1HashAsString(quarantinedDomainsSets.testSet2), + remotehash: computeSha1HashAsString(quarantinedDomainsSets.testSet2), + }); + + // Confirm that the updated quarantined domains list is now reflected + // by the results returned by WebExtensionPolicy.isQuarantinedURI. + // NOTE: Additional test coverage over the quarantined domains behaviors + // are part of a separate xpcshell test + // (see toolkit/components/extensions/test/xpcshell/test_QuarantinedDomains.js). + for (const domain of quarantinedDomainsSets.testSet2.split(",")) { + let uri = Services.io.newURI(`https://${domain}/`); + ok( + WebExtensionPolicy.isQuarantinedURI(uri), + `Expect ${domain} to be quarantined` + ); + } + + for (const domain of quarantinedDomainsSets.testSet1.split(",")) { + let uri = Services.io.newURI(`https://${domain}/`); + ok( + !WebExtensionPolicy.isQuarantinedURI(uri), + `Expect ${domain} to not be quarantined` + ); + } + + const NEW_PREF_VALUE = "newdomain1.org,newdomain2.org"; + await setAndEmitFakeRemoteSettingsData([ + { + // This entry doesn't includes an installTriggerDeprecation property + // (and then we verify that the pref is still set as expected). + id: "quarantinedDomains-withoutInstallTriggerDeprecation", + quarantinedDomains: { + [QUARANTINE_LIST_PREF]: NEW_PREF_VALUE, + }, + }, + ]); + assertQuarantinedListPref(NEW_PREF_VALUE); + assertQuarantinedListTelemetry({ + listhash: computeSha1HashAsString(NEW_PREF_VALUE), + remotehash: computeSha1HashAsString(NEW_PREF_VALUE), + }); + + await setAndEmitFakeRemoteSettingsData([ + { + // This entry includes an unexpected property + // (and then we verify that the pref is still set as expected). + id: "quarantinedDomains-withoutInstallTriggerDeprecation", + quarantinedDomains: { + [QUARANTINE_LIST_PREF]: quarantinedDomainsSets.testSet1, + }, + someUnexpectedProperty: "some unexpected value", + }, + ]); + assertQuarantinedListPref(quarantinedDomainsSets.testSet1); + assertQuarantinedListTelemetry({ + listhash: computeSha1HashAsString(quarantinedDomainsSets.testSet1), + remotehash: computeSha1HashAsString(quarantinedDomainsSets.testSet1), + }); + + info( + "Tamper with the domains list pref value, verify the remotesettings value is set back after restart" + ); + const MANUALLY_CHANGED_PREF_VALUE = + quarantinedDomainsSets.testSet1 + ",test123.example.org"; + Services.prefs.setStringPref( + QUARANTINE_LIST_PREF, + MANUALLY_CHANGED_PREF_VALUE + ); + // At this point we expect the value of the hash recorded in telemetry to differ + // between the listhash and remotehash glean metrics. + assertQuarantinedListTelemetry({ + listhash: computeSha1HashAsString(MANUALLY_CHANGED_PREF_VALUE), + remotehash: computeSha1HashAsString(quarantinedDomainsSets.testSet1), + }); + + // Then, we expect the remotehash and listhash to match each other again + // after the browser restart and the pref value to be back to the last + // value got from RemoteSettings. + info("Mock browser restart"); + // Clear telemetry data that was collected so far. + resetTelemetryData(); + const promisePrefChanged = TestUtils.waitForPrefChange(QUARANTINE_LIST_PREF); + await AddonTestUtils.promiseRestartManager(); + info( + `Wait for expected change notified for the ${QUARANTINE_LIST_PREF} pref` + ); + await promisePrefChanged; + + assertQuarantinedListPref(quarantinedDomainsSets.testSet1); + assertQuarantinedListTelemetry({ + listhash: computeSha1HashAsString(quarantinedDomainsSets.testSet1), + remotehash: computeSha1HashAsString(quarantinedDomainsSets.testSet1), + }); +} + +add_setup(async () => { + setupTelemetryForTests(); + await AddonTestUtils.promiseStartupManager(); + + Assert.ok( + QuarantinedDomains._initialized, + "QuarantinedDomains is initialized" + ); +}); + +add_task(testQuarantinedDomainsFromRemoteSettings); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_QuarantinedDomains_AddonWrapper.js b/toolkit/mozapps/extensions/test/xpcshell/test_QuarantinedDomains_AddonWrapper.js new file mode 100644 index 0000000000..46312b192b --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_QuarantinedDomains_AddonWrapper.js @@ -0,0 +1,207 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { QuarantinedDomains } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionPermissions.sys.mjs" +); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.usePrivilegedSignatures = id => id.startsWith("privileged"); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42", "42"); + +add_setup(async () => { + await AddonTestUtils.promiseStartupManager(); +}); + +function assertQuarantineIgnoredByUserPrefsRemoved() { + const { PREF_ADDONS_BRANCH_NAME } = QuarantinedDomains; + const prefBranch = Services.prefs.getBranch(PREF_ADDONS_BRANCH_NAME); + for (const prefSuffix of prefBranch.getChildList("")) { + Assert.equal( + prefBranch.getPrefType(prefSuffix), + prefBranch.PREF_INVALID, + `${PREF_ADDONS_BRANCH_NAME}${prefSuffix} pref should have been removed` + ); + } +} + +async function testQuarantineDomainsAddonWrapperProperties() { + // Make sure no extension is initially user exempted. + const prefBranch = Services.prefs.getBranch( + QuarantinedDomains.PREF_ADDONS_BRANCH_NAME + ); + for (const leafName of prefBranch.getChildList("")) { + Services.prefs.clearUserPref( + QuarantinedDomains.PREF_ADDONS_BRANCH_NAME + leafName + ); + } + + const REGULAR_EXT_ID = "regular@ext.id"; + const PRIVILEGE_EXT_ID = "privileged@ext.id"; + const RECOMMENDED_EXT_ID = "recommended@ext.id"; + + const regularExt = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + browser_specific_settings: { gecko: { id: REGULAR_EXT_ID } }, + }, + }); + + const privilegedExt = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + browser_specific_settings: { gecko: { id: PRIVILEGE_EXT_ID } }, + }, + }); + + const testStartTime = Date.now(); + // Keep the recommendations validity range used here in sync with + // the one used in test_recommendations.js. + const not_before = new Date(testStartTime - 3600000).toISOString(); + const not_after = new Date(testStartTime + 3600000).toISOString(); + const RECOMMENDATION_FILE_NAME = "mozilla-recommendation.json"; + const recommendedExt = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + browser_specific_settings: { gecko: { id: RECOMMENDED_EXT_ID } }, + }, + files: { + [RECOMMENDATION_FILE_NAME]: { + addon_id: RECOMMENDED_EXT_ID, + states: ["fake", "states"], + validity: { not_before, not_after }, + }, + }, + }); + + await regularExt.startup(); + await privilegedExt.startup(); + await recommendedExt.startup(); + + function assertAddonWrapperProps(addon, expectedProps) { + const expectedPropNames = Object.keys(expectedProps); + if (!expectedPropNames.length) { + throw new Error("expectedProps shouldn't be empty"); + } + for (const propName of expectedPropNames) { + Assert.deepEqual( + addon[propName], + expectedProps[propName], + `Got the expected value on ${propName} property from ${addon.id}` + ); + } + } + + const EXPECTED_PROPS_REGULAR_EXT = { + id: regularExt.id, + isPrivileged: false, + recommendationStates: [], + quarantineIgnoredByApp: false, + quarantineIgnoredByUser: false, + canChangeQuarantineIgnored: true, + }; + + const EXPECTED_PROPS_PRIVILEGED_EXT = { + id: privilegedExt.id, + isPrivileged: true, + recommendationStates: [], + // Expected to be true due to privileged signature. + quarantineIgnoredByApp: true, + quarantineIgnoredByUser: false, + // Expected to be false for app allowed. + canChangeQuarantineIgnored: false, + }; + + const EXPECTED_PROPS_RECOMMENDED_EXT = { + id: recommendedExt.id, + isPrivileged: false, + recommendationStates: ["fake", "states"], + // Expected to be true due to recommendationStates. + quarantineIgnoredByApp: true, + quarantineIgnoredByUser: false, + // Expected to be false for app allowed. + canChangeQuarantineIgnored: false, + }; + + assertAddonWrapperProps(regularExt.addon, EXPECTED_PROPS_REGULAR_EXT); + + assertAddonWrapperProps(privilegedExt.addon, EXPECTED_PROPS_PRIVILEGED_EXT); + + assertAddonWrapperProps(recommendedExt.addon, EXPECTED_PROPS_RECOMMENDED_EXT); + + info("Verify quarantineIgnoredByUser property changed"); + let promisePropChanged = + AddonTestUtils.promiseAddonEvent("onPropertyChanged"); + regularExt.addon.quarantineIgnoredByUser = true; + assertAddonWrapperProps(regularExt.addon, { + ...EXPECTED_PROPS_REGULAR_EXT, + quarantineIgnoredByUser: true, + }); + info("Wait for onPropertyChanged listener to be called"); + let [addon, props] = await promisePropChanged; + Assert.deepEqual( + { + addonId: addon.id, + props, + }, + { + addonId: regularExt.id, + props: ["quarantineIgnoredByUser"], + }, + "Got the expected params from onPropertyChanged listener call" + ); + Services.prefs.clearUserPref( + QuarantinedDomains.getUserAllowedAddonIdPrefName(regularExt.id) + ); + + info("Verify canChangeQuarantineIgnored on quarantineDomainsEnabled false"); + Services.prefs.setBoolPref("extensions.quarantinedDomains.enabled", false); + assertAddonWrapperProps(regularExt.addon, { + ...EXPECTED_PROPS_REGULAR_EXT, + canChangeQuarantineIgnored: false, + }); + Services.prefs.clearUserPref("extensions.quarantinedDomains.enabled"); + assertAddonWrapperProps(regularExt.addon, EXPECTED_PROPS_REGULAR_EXT); + + info("Verify canChangeQuarantineIgnored on uiDisabled true"); + Services.prefs.setBoolPref("extensions.quarantinedDomains.uiDisabled", true); + assertAddonWrapperProps(regularExt.addon, { + ...EXPECTED_PROPS_REGULAR_EXT, + canChangeQuarantineIgnored: false, + }); + Services.prefs.clearUserPref("extensions.quarantinedDomains.uiDisabled"); + assertAddonWrapperProps(regularExt.addon, EXPECTED_PROPS_REGULAR_EXT); + + info( + "Verify that the per-addon quarantineIgnoredByUser pref is removed on addon uninstall" + ); + + promisePropChanged = AddonTestUtils.promiseAddonEvent("onPropertyChanged"); + regularExt.addon.quarantineIgnoredByUser = true; + await promisePropChanged; + Assert.equal( + Services.prefs.getBoolPref( + QuarantinedDomains.getUserAllowedAddonIdPrefName(regularExt.id) + ), + true, + "Expect the per-addon quarantineIgnoredByUser to be set" + ); + + await recommendedExt.unload(); + await privilegedExt.unload(); + await regularExt.unload(); + + assertQuarantineIgnoredByUserPrefsRemoved(); +} + +add_task( + { + pref_set: [ + ["extensions.quarantinedDomains.enabled", true], + ["extensions.quarantinedDomains.uiDisabled", false], + ], + }, + testQuarantineDomainsAddonWrapperProperties +); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js b/toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js new file mode 100644 index 0000000000..97294bf6ed --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that we only check manifest age for disabled extensions + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +add_task(async function setup() { + await promiseStartupManager(); + registerCleanupFunction(promiseShutdownManager); + + await promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { gecko: { id: "enabled@tests.mozilla.org" } }, + }, + }); + await promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { + gecko: { id: "disabled@tests.mozilla.org" }, + }, + }, + }); + + let addon = await promiseAddonByID("disabled@tests.mozilla.org"); + notEqual(addon, null); + await addon.disable(); +}); + +// Keep track of the last time stamp we've used, so that we can keep moving +// it forward (if we touch two different files in the same add-on with the same +// timestamp we may not consider the change significant) +var lastTimestamp = Date.now(); + +/* + * Helper function to touch a file and then test whether we detect the change. + * @param XS The XPIState object. + * @param aPath File path to touch. + * @param aChange True if we should notice the change, False if we shouldn't. + */ +function checkChange(XS, aPath, aChange) { + Assert.ok(aPath.exists()); + lastTimestamp += 10000; + info("Touching file " + aPath.path + " with " + lastTimestamp); + aPath.lastModifiedTime = lastTimestamp; + Assert.equal(XS.scanForChanges(), aChange); + // Save the pref so we don't detect this change again + XS.save(); +} + +// Get a reference to the XPIState (loaded by startupManager) so we can unit test it. +function getXS() { + const { XPIExports } = ChromeUtils.importESModule( + "resource://gre/modules/addons/XPIExports.sys.mjs" + ); + return XPIExports.XPIInternal.XPIStates; +} + +async function getXSJSON() { + await AddonTestUtils.loadAddonsList(true); + + return aomStartup.readStartupData(); +} + +add_task(async function detect_touches() { + let XS = getXS(); + + // Should be no changes detected here, because everything should start out up-to-date. + Assert.ok(!XS.scanForChanges()); + + let states = XS.getLocation("app-profile"); + + // State should correctly reflect enabled/disabled + + let state = states.get("enabled@tests.mozilla.org"); + Assert.notEqual(state, null, "Found xpi state for enabled extension"); + Assert.ok(state.enabled, "enabled extension has correct xpi state"); + + state = states.get("disabled@tests.mozilla.org"); + Assert.notEqual(state, null, "Found xpi state for disabled extension"); + Assert.ok(!state.enabled, "disabled extension has correct xpi state"); + + // Touch various files and make sure the change is detected. + + // We notice that a packed XPI is touched for an enabled add-on. + let peFile = profileDir.clone(); + peFile.append("enabled@tests.mozilla.org.xpi"); + checkChange(XS, peFile, true); + + // We should notice the packed XPI change for a disabled add-on too. + let pdFile = profileDir.clone(); + pdFile.append("disabled@tests.mozilla.org.xpi"); + checkChange(XS, pdFile, true); +}); + +/* + * Uninstalling extensions should immediately remove them from XPIStates. + */ +add_task(async function uninstall_bootstrap() { + let pe = await promiseAddonByID("enabled@tests.mozilla.org"); + await pe.uninstall(); + + let xpiState = await getXSJSON(); + Assert.equal( + false, + "enabled@tests.mozilla.org" in xpiState["app-profile"].addons + ); +}); + +/* + * Installing an extension should immediately add it to XPIState + */ +add_task(async function install_bootstrap() { + const ID = "addon@tests.mozilla.org"; + let XS = getXS(); + + await promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { gecko: { id: ID } }, + }, + }); + let addon = await promiseAddonByID(ID); + + let xState = XS.getAddon("app-profile", ID); + Assert.ok(!!xState); + Assert.ok(xState.enabled); + Assert.equal(xState.mtime, addon.updateDate.getTime()); + await addon.uninstall(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_XPIcancel.js b/toolkit/mozapps/extensions/test/xpcshell/test_XPIcancel.js new file mode 100644 index 0000000000..9e6b2faa67 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_XPIcancel.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test the cancellable doing/done/cancelAll API in XPIProvider + +const { XPIExports } = ChromeUtils.importESModule( + "resource://gre/modules/addons/XPIExports.sys.mjs" +); + +function run_test() { + // Check that cancelling with nothing in progress doesn't blow up + XPIExports.XPIInstall.cancelAll(); + + // Check that a basic object gets cancelled + let getsCancelled = { + isCancelled: false, + cancel() { + if (this.isCancelled) { + do_throw("Already cancelled"); + } + this.isCancelled = true; + }, + }; + XPIExports.XPIInstall.doing(getsCancelled); + XPIExports.XPIInstall.cancelAll(); + Assert.ok(getsCancelled.isCancelled); + + // Check that if we complete a cancellable, it doesn't get cancelled + let doesntGetCancelled = { + cancel: () => do_throw("This should not have been cancelled"), + }; + XPIExports.XPIInstall.doing(doesntGetCancelled); + Assert.ok(XPIExports.XPIInstall.done(doesntGetCancelled)); + XPIExports.XPIInstall.cancelAll(); + + // A cancellable that adds a cancellable + getsCancelled.isCancelled = false; + let addsAnother = { + isCancelled: false, + cancel() { + if (this.isCancelled) { + do_throw("Already cancelled"); + } + this.isCancelled = true; + XPIExports.XPIInstall.doing(getsCancelled); + }, + }; + XPIExports.XPIInstall.doing(addsAnother); + XPIExports.XPIInstall.cancelAll(); + Assert.ok(addsAnother.isCancelled); + Assert.ok(getsCancelled.isCancelled); + + // A cancellable that removes another. This assumes that Set() iterates in the + // order that members were added + let removesAnother = { + isCancelled: false, + cancel() { + if (this.isCancelled) { + do_throw("Already cancelled"); + } + this.isCancelled = true; + XPIExports.XPIInstall.done(doesntGetCancelled); + }, + }; + XPIExports.XPIInstall.doing(removesAnother); + XPIExports.XPIInstall.doing(doesntGetCancelled); + XPIExports.XPIInstall.cancelAll(); + Assert.ok(removesAnother.isCancelled); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_addonStartup.js b/toolkit/mozapps/extensions/test/xpcshell/test_addonStartup.js new file mode 100644 index 0000000000..715b74a068 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_addonStartup.js @@ -0,0 +1,93 @@ +"use strict"; + +add_task(async function test_XPIStates_invalid_paths() { + let { path } = gAddonStartup; + + let startupDatasets = [ + { + "app-profile": { + addons: { + "xpcshell-something-or-other@mozilla.org": { + bootstrapped: true, + dependencies: [], + enabled: true, + hasEmbeddedWebExtension: false, + lastModifiedTime: 1, + path: "xpcshell-something-or-other@mozilla.org", + version: "0.0.0", + }, + }, + checkStartupModifications: true, + path: "/home/xpcshell/.mozilla/firefox/default/extensions", + }, + }, + { + "app-profile": { + addons: { + "xpcshell-something-or-other@mozilla.org": { + bootstrapped: true, + dependencies: [], + enabled: true, + hasEmbeddedWebExtension: false, + lastModifiedTime: 1, + path: "xpcshell-something-or-other@mozilla.org", + version: "0.0.0", + }, + }, + checkStartupModifications: true, + path: "c:\\Users\\XpcShell\\Application Data\\Mozilla Firefox\\Profiles\\meh", + }, + }, + { + "app-profile": { + addons: { + "xpcshell-something-or-other@mozilla.org": { + bootstrapped: true, + dependencies: [], + enabled: true, + hasEmbeddedWebExtension: false, + lastModifiedTime: 1, + path: "/home/xpcshell/my-extensions/something-or-other", + version: "0.0.0", + }, + }, + checkStartupModifications: true, + path: "/home/xpcshell/.mozilla/firefox/default/extensions", + }, + }, + { + "app-profile": { + addons: { + "xpcshell-something-or-other@mozilla.org": { + bootstrapped: true, + dependencies: [], + enabled: true, + hasEmbeddedWebExtension: false, + lastModifiedTime: 1, + path: "c:\\Users\\XpcShell\\my-extensions\\something-or-other", + version: "0.0.0", + }, + }, + checkStartupModifications: true, + path: "c:\\Users\\XpcShell\\Application Data\\Mozilla Firefox\\Profiles\\meh", + }, + }, + ]; + + for (let startupData of startupDatasets) { + await IOUtils.writeJSON(path, startupData, { compress: true }); + + try { + let result = aomStartup.readStartupData(); + info(`readStartupData() returned ${JSON.stringify(result)}`); + } catch (e) { + // We don't care if this throws, only that it doesn't crash. + info(`readStartupData() threw: ${e}`); + equal( + e.result, + Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH, + "Got expected error code" + ); + } + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_addon_manager_telemetry_events.js b/toolkit/mozapps/extensions/test/xpcshell/test_addon_manager_telemetry_events.js new file mode 100644 index 0000000000..6a533f540a --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_addon_manager_telemetry_events.js @@ -0,0 +1,1049 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const { TelemetryController } = ChromeUtils.importESModule( + "resource://gre/modules/TelemetryController.sys.mjs" +); +const { AMTelemetry } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); + +// We don't have an easy way to serve update manifests from a secure URL. +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); + +const EVENT_CATEGORY = "addonsManager"; +const EVENT_METHODS_INSTALL = ["install", "update"]; +const EVENT_METHODS_MANAGE = ["disable", "enable", "uninstall"]; +const EVENT_METHODS = [...EVENT_METHODS_INSTALL, ...EVENT_METHODS_MANAGE]; +const GLEAN_EVENT_NAMES = ["install", "update", "manage"]; + +const FAKE_INSTALL_TELEMETRY_INFO = { + source: "fake-install-source", + method: "fake-install-method", +}; + +add_setup(() => { + do_get_profile(); + Services.fog.initializeFOG(); +}); + +function getTelemetryEvents(includeMethods = EVENT_METHODS) { + const snapshot = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ); + + ok( + snapshot.parent && !!snapshot.parent.length, + "Got parent telemetry events in the snapshot" + ); + + return snapshot.parent + .filter(([timestamp, category, method]) => { + const includeMethod = includeMethods + ? includeMethods.includes(method) + : true; + + return category === EVENT_CATEGORY && includeMethod; + }) + .map(event => { + return { + method: event[2], + object: event[3], + value: event[4], + extra: event[5], + }; + }); +} + +function assertNoTelemetryEvents() { + const snapshot = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ); + + if (!snapshot.parent || snapshot.parent.length === 0) { + ok(true, "Got no parent telemetry events as expected"); + return; + } + + let filteredEvents = snapshot.parent.filter( + ([timestamp, category, method]) => { + return category === EVENT_CATEGORY; + } + ); + + Assert.deepEqual(filteredEvents, [], "Got no AMTelemetry events as expected"); +} + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + + // Thunderbird doesn't have one or more of the probes used in this test. + // Ensure the data is collected anyway. + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + // Telemetry test setup needed to ensure that the builtin events are defined + // and they can be collected and verified. + await TelemetryController.testSetup(); + + await promiseStartupManager(); +}); + +// Test the basic install and management flows. +add_task( + { + // We need to enable this pref because some assertions verify that + // `installOrigins` is collected in some Telemetry events. + pref_set: [["extensions.install_origins.enabled", true]], + }, + async function test_basic_telemetry_events() { + const EXTENSION_ID = "basic@test.extension"; + + const manifest = { + browser_specific_settings: { gecko: { id: EXTENSION_ID } }, + }; + + let extension = ExtensionTestUtils.loadExtension({ + manifest, + useAddonManager: "permanent", + amInstallTelemetryInfo: FAKE_INSTALL_TELEMETRY_INFO, + }); + + await extension.startup(); + + const addon = await promiseAddonByID(EXTENSION_ID); + + info("Disabling the extension"); + await addon.disable(); + + info("Set pending uninstall on the extension"); + const onceAddonUninstalling = promiseAddonEvent("onUninstalling"); + addon.uninstall(true); + await onceAddonUninstalling; + + info("Cancel pending uninstall"); + const oncePendingUninstallCancelled = promiseAddonEvent( + "onOperationCancelled" + ); + addon.cancelUninstall(); + await oncePendingUninstallCancelled; + + info("Re-enabling the extension"); + const onceAddonStarted = promiseWebExtensionStartup(EXTENSION_ID); + const onceAddonEnabled = promiseAddonEvent("onEnabled"); + addon.enable(); + await Promise.all([onceAddonEnabled, onceAddonStarted]); + + await extension.unload(); + + let amEvents = getTelemetryEvents(); + let gleanEvents = AddonTestUtils.getAMGleanEvents(GLEAN_EVENT_NAMES); + + const amMethods = amEvents.map(evt => evt.method); + const expectedMethods = [ + // These two install methods are related to the steps "started" and "completed". + "install", + "install", + // Sequence of disable and enable (pending uninstall and undo uninstall are not going to + // record any telemetry events). + "disable", + "enable", + // The final "uninstall" when the test extension is unloaded. + "uninstall", + ]; + Assert.deepEqual( + amMethods, + expectedMethods, + "Got the addonsManager telemetry events in the expected order" + ); + Assert.deepEqual( + expectedMethods, + gleanEvents.map(evt => { + // Install events don't have a method, so use ducktyping to recognize + // them: they have a step, but unlike update events, no updated_from. + if (evt.step && !evt.updated_from) { + return "install"; + } + return evt.method; + }), + "Got the addonsManager Glean events in the expected order." + ); + + const installEvents = amEvents.filter(evt => evt.method === "install"); + const expectedInstallEvents = [ + { + method: "install", + object: "extension", + value: "1", + extra: { + addon_id: "basic@test.extension", + step: "started", + install_origins: "0", + ...FAKE_INSTALL_TELEMETRY_INFO, + }, + }, + { + method: "install", + object: "extension", + value: "1", + extra: { + addon_id: "basic@test.extension", + step: "completed", + install_origins: "0", + ...FAKE_INSTALL_TELEMETRY_INFO, + }, + }, + ]; + Assert.deepEqual( + installEvents, + expectedInstallEvents, + "Got the expected addonsManager.install events" + ); + + let gleanInstall = { + addon_type: "extension", + addon_id: "basic@test.extension", + source: FAKE_INSTALL_TELEMETRY_INFO.source, + source_method: FAKE_INSTALL_TELEMETRY_INFO.method, + install_origins: "0", + }; + Assert.deepEqual( + AddonTestUtils.getAMGleanEvents("install"), + [ + { step: "started", ...gleanInstall }, + { step: "completed", ...gleanInstall }, + ], + "Got the expected addonsManager Glean events." + ); + + let gleanManage = { + addon_type: "extension", + addon_id: "basic@test.extension", + source: FAKE_INSTALL_TELEMETRY_INFO.source, + source_method: FAKE_INSTALL_TELEMETRY_INFO.method, + }; + Assert.deepEqual( + AddonTestUtils.getAMGleanEvents("manage"), + [ + { method: "disable", ...gleanManage }, + { method: "enable", ...gleanManage }, + { method: "uninstall", ...gleanManage }, + ], + "Got the expected addonsManager Glean events" + ); + + const manageEvents = amEvents.filter(evt => + EVENT_METHODS_MANAGE.includes(evt.method) + ); + const expectedExtra = FAKE_INSTALL_TELEMETRY_INFO; + const expectedManageEvents = [ + { + method: "disable", + object: "extension", + value: "basic@test.extension", + extra: expectedExtra, + }, + { + method: "enable", + object: "extension", + value: "basic@test.extension", + extra: expectedExtra, + }, + { + method: "uninstall", + object: "extension", + value: "basic@test.extension", + extra: expectedExtra, + }, + ]; + Assert.deepEqual( + manageEvents, + expectedManageEvents, + "Got the expected addonsManager.manage events" + ); + + Services.fog.testResetFOG(); + // Verify that on every install flow, the value of the addonsManager.install Telemetry events + // is being incremented. + + extension = ExtensionTestUtils.loadExtension({ + manifest, + useAddonManager: "permanent", + amInstallTelemetryInfo: FAKE_INSTALL_TELEMETRY_INFO, + }); + + await extension.startup(); + await extension.unload(); + + const eventsFromNewInstall = getTelemetryEvents(); + equal( + eventsFromNewInstall.length, + 3, + "Got the expected number of addonsManager install events" + ); + + equal( + 3, + AddonTestUtils.getAMGleanEvents(GLEAN_EVENT_NAMES).length, + "Got the expected number of addonsManager Glean events." + ); + equal( + 2, + AddonTestUtils.getAMGleanEvents("install", { install_id: "2" }).length, + "Got the expected install_id for Glean install event." + ); + + const eventValues = eventsFromNewInstall + .filter(evt => evt.method === "install") + .map(evt => evt.value); + const expectedValues = ["2", "2"]; + Assert.deepEqual( + eventValues, + expectedValues, + "Got the expected install id" + ); + + Services.fog.testResetFOG(); + } +); + +add_task( + { + // We need to enable this pref because some assertions verify that + // `installOrigins` is collected in some Telemetry events. + pref_set: [["extensions.install_origins.enabled", true]], + }, + async function test_update_telemetry_events() { + const EXTENSION_ID = "basic@test.extension"; + + const testserver = AddonTestUtils.createHttpServer({ + hosts: ["example.com"], + }); + + const updateUrl = `http://example.com/updates.json`; + + const testAddon = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + update_url: updateUrl, + }, + }, + }, + }); + + const testUserRequestedUpdate = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version: "2.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + update_url: updateUrl, + }, + }, + }, + }); + const testAppRequestedUpdate = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version: "2.1", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + update_url: updateUrl, + }, + }, + }, + }); + + testserver.registerFile( + `/addons/${EXTENSION_ID}-2.0.xpi`, + testUserRequestedUpdate + ); + testserver.registerFile( + `/addons/${EXTENSION_ID}-2.1.xpi`, + testAppRequestedUpdate + ); + + let updates = [ + { + version: "2.0", + update_link: `http://example.com/addons/${EXTENSION_ID}-2.0.xpi`, + applications: { + gecko: { + strict_min_version: "1", + }, + }, + }, + ]; + + testserver.registerPathHandler("/updates.json", (request, response) => { + response.write(`{ + "addons": { + "${EXTENSION_ID}": { + "updates": ${JSON.stringify(updates)} + } + } + }`); + }); + + await promiseInstallFile(testAddon, false, FAKE_INSTALL_TELEMETRY_INFO); + + let addon = await AddonManager.getAddonByID(EXTENSION_ID); + + // User requested update. + await promiseFindAddonUpdates( + addon, + AddonManager.UPDATE_WHEN_USER_REQUESTED + ); + let installs = await AddonManager.getAllInstalls(); + await promiseCompleteAllInstalls(installs); + + updates = [ + { + version: "2.1", + update_link: `http://example.com/addons/${EXTENSION_ID}-2.1.xpi`, + applications: { + gecko: { + strict_min_version: "1", + }, + }, + }, + ]; + + // App requested update. + await promiseFindAddonUpdates( + addon, + AddonManager.UPDATE_WHEN_PERIODIC_UPDATE + ); + let installs2 = await AddonManager.getAllInstalls(); + await promiseCompleteAllInstalls(installs2); + + updates = [ + { + version: "2.1.1", + update_link: `http://example.com/addons/${EXTENSION_ID}-2.1.1-network-failure.xpi`, + applications: { + gecko: { + strict_min_version: "1", + }, + }, + }, + ]; + + // Update which fails to download. + await promiseFindAddonUpdates( + addon, + AddonManager.UPDATE_WHEN_PERIODIC_UPDATE + ); + let installs3 = await AddonManager.getAllInstalls(); + await promiseCompleteAllInstalls(installs3); + + let amEvents = getTelemetryEvents(); + + const installEvents = amEvents + .filter(evt => evt.method === "install") + .map(evt => { + delete evt.value; + return evt; + }); + + const addon_id = "basic@test.extension"; + const object = "extension"; + + let gleanInstall = { + addon_id, + addon_type: "extension", + install_origins: "0", + source: FAKE_INSTALL_TELEMETRY_INFO.source, + source_method: FAKE_INSTALL_TELEMETRY_INFO.method, + }; + + Assert.deepEqual( + AddonTestUtils.getAMGleanEvents("install"), + [ + { step: "started", ...gleanInstall }, + { step: "completed", ...gleanInstall }, + ], + "Got the expected install Glean events." + ); + + Assert.deepEqual( + installEvents, + [ + { + method: "install", + object, + extra: { + addon_id, + step: "started", + install_origins: "0", + ...FAKE_INSTALL_TELEMETRY_INFO, + }, + }, + { + method: "install", + object, + extra: { + addon_id, + step: "completed", + install_origins: "0", + ...FAKE_INSTALL_TELEMETRY_INFO, + }, + }, + ], + "Got the expected addonsManager.install events" + ); + + const updateEvents = amEvents + .filter(evt => evt.method === "update") + .map(evt => { + delete evt.value; + return evt; + }); + + const method = "update"; + const baseExtra = FAKE_INSTALL_TELEMETRY_INFO; + + let glean = AddonTestUtils.getAMGleanEvents("update"); + glean.forEach(e => delete e.download_time); + + let gleanUpdate = { + addon_id, + addon_type: "extension", + source: FAKE_INSTALL_TELEMETRY_INFO.source, + source_method: FAKE_INSTALL_TELEMETRY_INFO.method, + }; + Assert.deepEqual( + glean, + [ + { step: "started", updated_from: "user", ...gleanUpdate }, + { step: "download_started", updated_from: "user", ...gleanUpdate }, + { step: "download_completed", updated_from: "user", ...gleanUpdate }, + { step: "completed", updated_from: "user", ...gleanUpdate }, + { step: "started", updated_from: "app", ...gleanUpdate }, + { step: "download_started", updated_from: "app", ...gleanUpdate }, + { step: "download_completed", updated_from: "app", ...gleanUpdate }, + { step: "completed", updated_from: "app", ...gleanUpdate }, + { step: "started", updated_from: "app", ...gleanUpdate }, + { step: "download_started", updated_from: "app", ...gleanUpdate }, + { + step: "download_failed", + updated_from: "app", + error: "ERROR_NETWORK_FAILURE", + ...gleanUpdate, + }, + ], + "Got the expected Glean update events." + ); + + const expectedUpdateEvents = [ + // User-requested update to the 2.1 version. + { + method, + object, + extra: { + ...baseExtra, + addon_id, + step: "started", + updated_from: "user", + }, + }, + { + method, + object, + extra: { + ...baseExtra, + addon_id, + step: "download_started", + updated_from: "user", + }, + }, + { + method, + object, + extra: { + ...baseExtra, + addon_id, + step: "download_completed", + updated_from: "user", + }, + }, + { + method, + object, + extra: { + ...baseExtra, + addon_id, + step: "completed", + updated_from: "user", + }, + }, + // App-requested update to the 2.1 version. + { + method, + object, + extra: { ...baseExtra, addon_id, step: "started", updated_from: "app" }, + }, + { + method, + object, + extra: { + ...baseExtra, + addon_id, + step: "download_started", + updated_from: "app", + }, + }, + { + method, + object, + extra: { + ...baseExtra, + addon_id, + step: "download_completed", + updated_from: "app", + }, + }, + { + method, + object, + extra: { + ...baseExtra, + addon_id, + step: "completed", + updated_from: "app", + }, + }, + // Broken update to the 2.1.1 version (on ERROR_NETWORK_FAILURE). + { + method, + object, + extra: { ...baseExtra, addon_id, step: "started", updated_from: "app" }, + }, + { + method, + object, + extra: { + ...baseExtra, + addon_id, + step: "download_started", + updated_from: "app", + }, + }, + { + method, + object, + extra: { + ...baseExtra, + addon_id, + error: "ERROR_NETWORK_FAILURE", + step: "download_failed", + updated_from: "app", + }, + }, + ]; + + AddonTestUtils.getAMGleanEvents("update") + .filter(e => ["download_completed", "download_failed"].includes(e.step)) + .forEach(e => + Assert.greater( + parseInt(e.download_time, 10), + 0, + `At step ${e.step} download_time: ${e.download_time}` + ) + ); + + for (let i = 0; i < updateEvents.length; i++) { + if ( + ["download_completed", "download_failed"].includes( + updateEvents[i].extra.step + ) + ) { + const download_time = parseInt(updateEvents[i].extra.download_time, 10); + ok( + !isNaN(download_time) && download_time > 0, + `Got a download_time extra in ${updateEvents[i].extra.step} events: ${download_time}` + ); + + delete updateEvents[i].extra.download_time; + } + + Assert.deepEqual( + updateEvents[i], + expectedUpdateEvents[i], + "Got the expected addonsManager.update events" + ); + } + + equal( + updateEvents.length, + expectedUpdateEvents.length, + "Got the expected number of addonsManager.update events" + ); + + await addon.uninstall(); + + // Clear any AMTelemetry events related to the uninstalled extensions. + getTelemetryEvents(); + Services.fog.testResetFOG(); + } +); + +add_task(async function test_no_telemetry_events_on_internal_sources() { + assertNoTelemetryEvents(); + + const INTERNAL_EXTENSION_ID = "internal@test.extension"; + + // Install an extension which has internal as its installation source, + // and expect it to do not appear in the collected telemetry events. + let internalExtension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id: INTERNAL_EXTENSION_ID } }, + }, + useAddonManager: "permanent", + amInstallTelemetryInfo: { source: "internal" }, + }); + + await internalExtension.startup(); + + const internalAddon = await promiseAddonByID(INTERNAL_EXTENSION_ID); + + info("Disabling the internal extension"); + const onceInternalAddonDisabled = promiseAddonEvent("onDisabled"); + internalAddon.disable(); + await onceInternalAddonDisabled; + + info("Re-enabling the internal extension"); + const onceInternalAddonStarted = promiseWebExtensionStartup( + INTERNAL_EXTENSION_ID + ); + const onceInternalAddonEnabled = promiseAddonEvent("onEnabled"); + internalAddon.enable(); + await Promise.all([onceInternalAddonEnabled, onceInternalAddonStarted]); + + await internalExtension.unload(); + + assertNoTelemetryEvents(); +}); + +add_task(async function test_collect_attribution_data_for_amo() { + assertNoTelemetryEvents(); + + // We pass the `source` value to `amInstallTelemetryInfo` in this test so the + // host could be anything in this variable below. Whether to collect + // attribution data for AMO is determined by the `source` value, not this + // host. + const url = "https://addons.mozilla.org/"; + const addonId = "{28374a9a-676c-5640-bfa7-865cd4686ead}"; + // This is the SHA256 hash of the `addonId` above. + const expectedHashedAddonId = + "cf815c9f45c249473d630705f89e64d359737a106a375bbb83be71e6d52dc234"; + + for (const { source, sourceURL, expectNoEvent, expectedAmoAttribution } of [ + // Basic test. + { + source: "amo", + sourceURL: `${url}?utm_content=utm-content-value`, + expectedAmoAttribution: { + utm_content: "utm-content-value", + }, + }, + // No UTM parameters will produce an event without any attribution data. + { + source: "amo", + sourceURL: url, + expectedAmoAttribution: {}, + }, + // Invalid source URLs will produce an event without any attribution data. + { + source: "amo", + sourceURL: "invalid-url", + expectedAmoAttribution: {}, + }, + // No source URL. + { + source: "amo", + sourceURL: null, + expectedAmoAttribution: {}, + }, + { + source: "amo", + sourceURL: undefined, + expectedAmoAttribution: {}, + }, + // Ignore unsupported/bogus UTM parameters. + { + source: "amo", + sourceURL: [ + `${url}?utm_content=utm-content-value`, + "utm_foo=invalid", + "utm_campaign=some-campaign", + "utm_term=invalid-too", + ].join("&"), + expectedAmoAttribution: { + utm_campaign: "some-campaign", + utm_content: "utm-content-value", + }, + }, + { + source: "amo", + sourceURL: `${url}?foo=bar&q=azerty`, + expectedAmoAttribution: {}, + }, + // Long values are truncated. + { + source: "amo", + sourceURL: `${url}?utm_medium=${"a".repeat(100)}`, + expectedAmoAttribution: { + utm_medium: "a".repeat(40), + }, + }, + // Only collect the first value if the parameter is passed more than once. + { + source: "amo", + sourceURL: `${url}?utm_source=first-source&utm_source=second-source`, + expectedAmoAttribution: { + utm_source: "first-source", + }, + }, + // When source is "disco", we don't collect the UTM parameters. + { + source: "disco", + sourceURL: `${url}?utm_content=utm-content-value`, + expectedAmoAttribution: {}, + }, + // When source is neither "amo" nor "disco", we don't collect anything. + { + source: "link", + sourceURL: `${url}?utm_content=utm-content-value`, + expectNoEvent: true, + }, + { + source: null, + sourceURL: `${url}?utm_content=utm-content-value`, + expectNoEvent: true, + }, + { + source: undefined, + sourceURL: `${url}?utm_content=utm-content-value`, + expectNoEvent: true, + }, + ]) { + const extDefinition = { + useAddonManager: "permanent", + manifest: { + version: "1.0", + browser_specific_settings: { gecko: { id: addonId } }, + }, + amInstallTelemetryInfo: { + ...FAKE_INSTALL_TELEMETRY_INFO, + sourceURL, + source, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extDefinition); + + await extension.startup(); + + const installStatsEvents = getTelemetryEvents(["install_stats"]); + let gleanEvents = AddonTestUtils.getAMGleanEvents(["installStats"]); + Services.fog.testResetFOG(); + + if (expectNoEvent === true) { + Assert.equal( + installStatsEvents.length, + 0, + "no install_stats event should be recorded" + ); + Assert.equal( + gleanEvents.length, + 0, + "No install_stats Glean event should be recorded." + ); + } else { + Assert.equal( + installStatsEvents.length, + 1, + "only one install_stats event should be recorded" + ); + Assert.equal( + gleanEvents.length, + 1, + "Only one install_stats Glean event should be recorded." + ); + + const installStatsEvent = installStatsEvents[0]; + + Assert.deepEqual(installStatsEvent, { + method: "install_stats", + object: "extension", + value: expectedHashedAddonId, + extra: { + addon_id: addonId, + ...expectedAmoAttribution, + }, + }); + Assert.deepEqual(gleanEvents[0], { + addon_id: addonId, + addon_type: "extension", + ...expectedAmoAttribution, + }); + } + + await extension.upgrade({ + ...extDefinition, + manifest: { + ...extDefinition.manifest, + version: "2.0", + }, + }); + + Assert.deepEqual( + getTelemetryEvents(["install_stats"]), + [], + "no install_stats event should be recorded on addon updates" + ); + + Assert.deepEqual( + AddonTestUtils.getAMGleanEvents(["installStats"]), + [], + "No install_stats Glean event should be recorded on addon updates." + ); + + await extension.unload(); + Services.fog.testResetFOG(); + } + + getTelemetryEvents(); +}); + +add_task(async function test_collect_attribution_data_for_amo_with_long_id() { + assertNoTelemetryEvents(); + + // We pass the `source` value to `installTelemetryInfo` in this test so the + // host could be anything in this variable below. Whether to collect + // attribution data for AMO is determined by the `source` value, not this + // host. + const url = "https://addons.mozilla.org/"; + const addonId = `@${"a".repeat(90)}`; + // This is the SHA256 hash of the `addonId` above. + const expectedHashedAddonId = + "964d902353fc1c127228b66ec8a174c340cb2e02dbb550c6000fb1cd3ca2f489"; + + const installTelemetryInfo = { + ...FAKE_INSTALL_TELEMETRY_INFO, + sourceURL: `${url}?utm_content=utm-content-value`, + source: "amo", + }; + + // We call `recordInstallStatsEvent()` directly because using an add-on ID + // longer than 64 chars causes signing issues in tests (because of the + // differences between the fake CertDB injected by + // `AddonTestUtils.overrideCertDB()` and the real one). + const fakeAddonInstall = { + addon: { id: addonId }, + type: "extension", + installTelemetryInfo, + hashedAddonId: expectedHashedAddonId, + }; + AMTelemetry.recordInstallStatsEvent(fakeAddonInstall); + + const installStatsEvents = getTelemetryEvents(["install_stats"]); + Assert.equal( + installStatsEvents.length, + 1, + "only one install_stats event should be recorded" + ); + + const installStatsEvent = installStatsEvents[0]; + + Assert.deepEqual(installStatsEvent, { + method: "install_stats", + object: "extension", + value: expectedHashedAddonId, + extra: { + addon_id: AMTelemetry.getTrimmedString(addonId), + utm_content: "utm-content-value", + }, + }); + + Assert.deepEqual( + AddonTestUtils.getAMGleanEvents(["installStats"]), + [ + { + addon_type: "extension", + addon_id: AMTelemetry.getTrimmedString(addonId), + utm_content: "utm-content-value", + }, + ], + "Got the expected install_stats Glean event." + ); + Services.fog.testResetFOG(); +}); + +add_task(async function test_collect_attribution_data_for_rtamo() { + assertNoTelemetryEvents(); + + const url = "https://addons.mozilla.org/"; + const addonId = "{28374a9a-676c-5640-bfa7-865cd4686ead}"; + // This is the SHA256 hash of the `addonId` above. + const expectedHashedAddonId = + "cf815c9f45c249473d630705f89e64d359737a106a375bbb83be71e6d52dc234"; + + // We simulate what is happening in: + // https://searchfox.org/mozilla-central/rev/d2786d9a6af7507bc3443023f0495b36b7e84c2d/browser/components/newtab/content-src/lib/aboutwelcome-utils.js#91 + const installTelemetryInfo = { + ...FAKE_INSTALL_TELEMETRY_INFO, + sourceURL: `${url}?utm_content=utm-content-value`, + source: "rtamo", + }; + + const fakeAddonInstall = { + addon: { id: addonId }, + type: "extension", + installTelemetryInfo, + hashedAddonId: expectedHashedAddonId, + }; + AMTelemetry.recordInstallStatsEvent(fakeAddonInstall); + + const installStatsEvents = getTelemetryEvents(["install_stats"]); + Assert.equal( + installStatsEvents.length, + 1, + "only one install_stats event should be recorded" + ); + + const installStatsEvent = installStatsEvents[0]; + + Assert.deepEqual(installStatsEvent, { + method: "install_stats", + object: "extension", + value: expectedHashedAddonId, + extra: { + addon_id: AMTelemetry.getTrimmedString(addonId), + }, + }); + + Assert.deepEqual( + AddonTestUtils.getAMGleanEvents(["installStats"]), + [ + { + addon_type: "extension", + addon_id: AMTelemetry.getTrimmedString(addonId), + }, + ], + "Got the expected install_stats Glean event." + ); + Services.fog.testResetFOG(); +}); + +add_task(async function teardown() { + await TelemetryController.testShutdown(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_amo_stats_telemetry.js b/toolkit/mozapps/extensions/test/xpcshell/test_amo_stats_telemetry.js new file mode 100644 index 0000000000..8176c5881a --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_amo_stats_telemetry.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { TelemetryController } = ChromeUtils.importESModule( + "resource://gre/modules/TelemetryController.sys.mjs" +); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "42" +); + +add_task(async function setup() { + // We need to set this pref to `true` in order to collect add-ons Telemetry + // data (which is considered extended data and disabled in CI). + const overridePreReleasePref = "toolkit.telemetry.testing.overridePreRelease"; + let oldOverrideValue = Services.prefs.getBoolPref( + overridePreReleasePref, + false + ); + Services.prefs.setBoolPref(overridePreReleasePref, true); + registerCleanupFunction(() => { + Services.prefs.setBoolPref(overridePreReleasePref, oldOverrideValue); + }); + + await TelemetryController.testSetup(); + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function test_ping_payload_and_environment() { + const extensions = [ + { + id: "addons-telemetry@test-extension-1", + name: "some extension 1", + version: "1.2.3", + }, + { + id: "addons-telemetry@test-extension-2", + name: "some extension 2", + version: "0.1", + }, + ]; + + // Install some extensions. + const installedExtensions = []; + for (const { id, name, version } of extensions) { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + name, + version, + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "permanent", + }); + installedExtensions.push(extension); + + await extension.startup(); + } + + const { payload, environment } = TelemetryController.getCurrentPingData(); + + // Important: `payload.info.addons` is being used for AMO usage stats. + Assert.ok("addons" in payload.info, "payload.info.addons is defined"); + Assert.equal( + payload.info.addons, + extensions + .map(({ id, version }) => `${encodeURIComponent(id)}:${version}`) + .join(",") + ); + Assert.ok( + "XPI" in payload.addonDetails, + "payload.addonDetails.XPI is defined" + ); + for (const { id, name } of extensions) { + Assert.ok(id in payload.addonDetails.XPI); + Assert.equal(payload.addonDetails.XPI[id].name, name); + } + + const { addons } = environment; + Assert.ok( + "activeAddons" in addons, + "environment.addons.activeAddons is defined" + ); + Assert.ok("theme" in addons, "environment.addons.theme is defined"); + for (const { id } of extensions) { + Assert.ok(id in environment.addons.activeAddons); + } + + for (const extension of installedExtensions) { + await extension.unload(); + } +}); + +add_task(async function cleanup() { + await TelemetryController.testShutdown(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_aom_startup.js b/toolkit/mozapps/extensions/test/xpcshell/test_aom_startup.js new file mode 100644 index 0000000000..f59a14dbbc --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_aom_startup.js @@ -0,0 +1,189 @@ +"use strict"; + +const { JSONFile } = ChromeUtils.importESModule( + "resource://gre/modules/JSONFile.sys.mjs" +); + +const aomStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"].getService( + Ci.amIAddonManagerStartup +); + +const gProfDir = do_get_profile(); + +Services.prefs.setIntPref( + "extensions.enabledScopes", + AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION +); +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42.0", "42.0"); + +const DUMMY_ID = "@dummy"; +const DUMMY_ADDONS = { + addons: { + "@dummy": { + lastModifiedTime: 1337, + rootURI: "resource:///modules/themes/dummy/", + version: "1", + }, + }, +}; + +const TEST_ADDON_ID = "@test-theme"; +const TEST_THEME = { + lastModifiedTime: 1337, + rootURI: "resource:///modules/themes/test/", + version: "1", +}; + +const TEST_ADDONS = { + addons: { + "@test-theme": TEST_THEME, + }, +}; + +// Utility to write out various addonStartup.json files. +async function writeAOMStartupData(data) { + let jsonFile = new JSONFile({ + path: PathUtils.join(gProfDir.path, "addonStartup.json.lz4"), + compression: "lz4", + }); + jsonFile.data = data; + await jsonFile._save(); + return aomStartup.readStartupData(); +} + +// This tests that any buitin removed from the build will +// get removed from the state data. +add_task(async function test_startup_missing_builtin() { + let startupData = await writeAOMStartupData({ + "app-builtin": DUMMY_ADDONS, + }); + Assert.ok( + !!startupData["app-builtin"].addons[DUMMY_ID], + "non-existant addon is in startup data" + ); + + await AddonTestUtils.promiseStartupManager(); + await AddonTestUtils.promiseShutdownManager(); + + // This data is flushed on shutdown, so we check it after shutdown. + startupData = aomStartup.readStartupData(); + Assert.equal( + startupData["app-builtin"].addons[DUMMY_ID], + undefined, + "non-existant addon is removed from startup data" + ); +}); + +// This test verifies that a builtin installed prior to the +// second scan is not overwritten by old state data during +// the scan. +add_task(async function test_startup_default_theme_moved() { + let startupData = await writeAOMStartupData({ + "app-profile": DUMMY_ADDONS, + "app-builtin": TEST_ADDONS, + }); + Assert.ok( + !!startupData["app-profile"].addons[DUMMY_ID], + "non-existant addon is in startup data" + ); + Assert.ok( + !!startupData["app-builtin"].addons[TEST_ADDON_ID], + "test addon is in startup data" + ); + + let themeDef = { + manifest: { + browser_specific_settings: { gecko: { id: TEST_ADDON_ID } }, + version: "1.1", + theme: {}, + }, + }; + + await setupBuiltinExtension(themeDef, "second-loc"); + await AddonTestUtils.promiseStartupManager("44"); + await AddonManager.maybeInstallBuiltinAddon( + TEST_ADDON_ID, + "1.1", + "resource://second-loc/" + ); + await AddonManagerPrivate.getNewSideloads(); + + let addon = await AddonManager.getAddonByID(TEST_ADDON_ID); + Assert.ok(!addon.foreignInstall, "addon was not marked as a foreign install"); + Assert.equal(addon.version, "1.1", "addon version is correct"); + + await AddonTestUtils.promiseShutdownManager(); + + // This data is flushed on shutdown, so we check it after shutdown. + startupData = aomStartup.readStartupData(); + Assert.equal( + startupData["app-builtin"].addons[TEST_ADDON_ID].version, + "1.1", + "startup data is correct in cache" + ); + Assert.equal( + startupData["app-builtin"].addons[DUMMY_ID], + undefined, + "non-existant addon is removed from startup data" + ); +}); + +// This test verifies that a builtin addon being updated +// is not marked as a foreignInstall. +add_task(async function test_startup_builtin_not_foreign() { + let startupData = await writeAOMStartupData({ + "app-profile": DUMMY_ADDONS, + "app-builtin": { + addons: { + "@test-theme": { + ...TEST_THEME, + rootURI: "resource://second-loc/", + }, + }, + }, + }); + Assert.ok( + !!startupData["app-profile"].addons[DUMMY_ID], + "non-existant addon is in startup data" + ); + Assert.ok( + !!startupData["app-builtin"].addons[TEST_ADDON_ID], + "test addon is in startup data" + ); + + let themeDef = { + manifest: { + browser_specific_settings: { gecko: { id: TEST_ADDON_ID } }, + version: "1.1", + theme: {}, + }, + }; + + await setupBuiltinExtension(themeDef, "second-loc"); + await AddonTestUtils.promiseStartupManager("43"); + await AddonManager.maybeInstallBuiltinAddon( + TEST_ADDON_ID, + "1.1", + "resource://second-loc/" + ); + await AddonManagerPrivate.getNewSideloads(); + + let addon = await AddonManager.getAddonByID(TEST_ADDON_ID); + Assert.ok(!addon.foreignInstall, "addon was not marked as a foreign install"); + Assert.equal(addon.version, "1.1", "addon version is correct"); + + await AddonTestUtils.promiseShutdownManager(); + + // This data is flushed on shutdown, so we check it after shutdown. + startupData = aomStartup.readStartupData(); + Assert.equal( + startupData["app-builtin"].addons[TEST_ADDON_ID].version, + "1.1", + "startup data is correct in cache" + ); + Assert.equal( + startupData["app-builtin"].addons[DUMMY_ID], + undefined, + "non-existant addon is removed from startup data" + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bad_json.js b/toolkit/mozapps/extensions/test/xpcshell/test_bad_json.js new file mode 100644 index 0000000000..ec6c30bd52 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_bad_json.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that we rebuild the database correctly if it contains +// JSON data that parses correctly but doesn't contain required fields + +add_task(async function () { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + + await promiseStartupManager(); + + const ID = "addon@tests.mozilla.org"; + await promiseInstallWebExtension({ + manifest: { + version: "2.0", + browser_specific_settings: { gecko: { id: ID } }, + }, + }); + + await promiseShutdownManager(); + + // First startup/shutdown finished + // Replace the JSON store with something bogus + await IOUtils.writeJSON(gExtensionsJSON.path, { + not: "what we expect to find", + }); + + await promiseStartupManager(); + // Retrieve an addon to force the database to rebuild + let addon = await AddonManager.getAddonByID(ID); + + Assert.equal(addon.id, ID); + + await promiseShutdownManager(); + + // Make sure our JSON database has schemaVersion and our installed extension + let data = await IOUtils.readJSON(gExtensionsJSON.path); + Assert.ok("schemaVersion" in data); + Assert.equal(data.addons[0].id, ID); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_badschema.js b/toolkit/mozapps/extensions/test/xpcshell/test_badschema.js new file mode 100644 index 0000000000..0fc810bf91 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_badschema.js @@ -0,0 +1,237 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Checks that we rebuild something sensible from a database with a bad schema + +var testserver = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); + +// register files with server +testserver.registerDirectory("/data/", do_get_file("data")); + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); + +const ADDONS = { + "addon1@tests.mozilla.org": { + manifest: { + name: "Test 1", + browser_specific_settings: { + gecko: { + id: "addon1@tests.mozilla.org", + strict_min_version: "2", + strict_max_version: "2", + }, + }, + }, + desiredValues: { + isActive: true, + userDisabled: false, + appDisabled: false, + pendingOperations: 0, + }, + }, + + "addon2@tests.mozilla.org": { + manifest: { + name: "Test 2", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "addon2@tests.mozilla.org", + }, + }, + }, + initialState: { + userDisabled: true, + }, + desiredValues: { + isActive: false, + userDisabled: true, + appDisabled: false, + pendingOperations: 0, + }, + }, + + "addon3@tests.mozilla.org": { + manifest: { + name: "Test 3", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "addon3@tests.mozilla.org", + update_url: "http://example.com/data/test_corrupt.json", + strict_min_version: "1", + strict_max_version: "1", + }, + }, + }, + findUpdates: true, + desiredValues: { + isActive: true, + userDisabled: false, + appDisabled: false, + pendingOperations: 0, + }, + }, + + "addon4@tests.mozilla.org": { + manifest: { + name: "Test 4", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "addon4@tests.mozilla.org", + update_url: "http://example.com/data/test_corrupt.json", + strict_min_version: "1", + strict_max_version: "1", + }, + }, + }, + initialState: { + userDisabled: true, + }, + findUpdates: true, + desiredValues: { + isActive: false, + userDisabled: true, + appDisabled: false, + pendingOperations: 0, + }, + }, + + "addon5@tests.mozilla.org": { + manifest: { + name: "Test 5", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "addon5@tests.mozilla.org", + strict_min_version: "1", + strict_max_version: "1", + }, + }, + }, + desiredValues: { + isActive: false, + userDisabled: false, + appDisabled: true, + pendingOperations: 0, + }, + }, + + "theme1@tests.mozilla.org": { + manifest: { + manifest_version: 2, + name: "Theme 1", + version: "1.0", + theme: { images: { theme_frame: "example.png" } }, + browser_specific_settings: { + gecko: { + id: "theme1@tests.mozilla.org", + }, + }, + }, + desiredValues: { + isActive: false, + userDisabled: true, + appDisabled: false, + pendingOperations: 0, + }, + }, + + "theme2@tests.mozilla.org": { + manifest: { + manifest_version: 2, + name: "Theme 2", + version: "1.0", + theme: { images: { theme_frame: "example.png" } }, + browser_specific_settings: { + gecko: { + id: "theme2@tests.mozilla.org", + }, + }, + }, + initialState: { + userDisabled: false, + }, + desiredValues: { + isActive: true, + userDisabled: false, + appDisabled: false, + pendingOperations: 0, + }, + }, +}; + +const IDS = Object.keys(ADDONS); + +function promiseUpdates(addon) { + return new Promise(resolve => { + addon.findUpdates( + { onUpdateFinished: resolve }, + AddonManager.UPDATE_WHEN_PERIODIC_UPDATE + ); + }); +} + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2"); + + for (let addon of Object.values(ADDONS)) { + let webext = createTempWebExtensionFile({ manifest: addon.manifest }); + await AddonTestUtils.manuallyInstall(webext); + } + + await promiseStartupManager(); + + let addons = await getAddons(IDS); + for (let [id, addon] of Object.entries(ADDONS)) { + if (addon.initialState) { + await setInitialState(addons.get(id), addon.initialState); + } + if (addon.findUpdates) { + await promiseUpdates(addons.get(id)); + } + } +}); + +add_task(async function test_after_restart() { + await promiseRestartManager(); + + info("Test add-on state after restart"); + let addons = await getAddons(IDS); + for (let [id, addon] of Object.entries(ADDONS)) { + checkAddon(id, addons.get(id), addon.desiredValues); + } + + await promiseShutdownManager(); +}); + +add_task(async function test_after_schema_version_change() { + // After restarting the database won't be open so we can alter + // the schema + await changeXPIDBVersion(100); + + await promiseStartupManager(); + + info("Test add-on state after schema version change"); + let addons = await getAddons(IDS); + for (let [id, addon] of Object.entries(ADDONS)) { + checkAddon(id, addons.get(id), addon.desiredValues); + } + + await promiseShutdownManager(); +}); + +add_task(async function test_after_second_restart() { + await promiseStartupManager(); + + info("Test add-on state after second restart"); + let addons = await getAddons(IDS); + for (let [id, addon] of Object.entries(ADDONS)) { + checkAddon(id, addons.get(id), addon.desiredValues); + } + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug587088.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug587088.js new file mode 100644 index 0000000000..261ef61807 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug587088.js @@ -0,0 +1,194 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This test is currently dead code. +/* eslint-disable */ + +// Tests that trying to upgrade or uninstall an extension that has a file locked +// will roll back the upgrade or uninstall and retry at the next restart + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +const ADDONS = [ + { + "install.rdf": { + id: "addon1@tests.mozilla.org", + version: "1.0", + name: "Bug 587088 Test", + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1", + }, + ], + }, + testfile: "", + testfile1: "", + }, + + { + "install.rdf": { + id: "addon1@tests.mozilla.org", + version: "2.0", + name: "Bug 587088 Test", + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1", + }, + ], + }, + testfile: "", + testfile2: "", + }, +]; + +add_task(async function setup() { + // This is only an issue on windows. + if (!("nsIWindowsRegKey" in Ci)) return; + + do_test_pending(); + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); +}); + +function check_addon(aAddon, aVersion) { + Assert.notEqual(aAddon, null); + Assert.equal(aAddon.version, aVersion); + Assert.ok(aAddon.isActive); + Assert.ok(isExtensionInAddonsList(profileDir, aAddon.id)); + + Assert.ok(aAddon.hasResource("testfile")); + if (aVersion == "1.0") { + Assert.ok(aAddon.hasResource("testfile1")); + Assert.ok(!aAddon.hasResource("testfile2")); + } else { + Assert.ok(!aAddon.hasResource("testfile1")); + Assert.ok(aAddon.hasResource("testfile2")); + } + + Assert.equal(aAddon.pendingOperations, AddonManager.PENDING_NONE); +} + +function check_addon_upgrading(aAddon) { + Assert.notEqual(aAddon, null); + Assert.equal(aAddon.version, "1.0"); + Assert.ok(aAddon.isActive); + Assert.ok(isExtensionInAddonsList(profileDir, aAddon.id)); + + Assert.ok(aAddon.hasResource("testfile")); + Assert.ok(aAddon.hasResource("testfile1")); + Assert.ok(!aAddon.hasResource("testfile2")); + + Assert.equal(aAddon.pendingOperations, AddonManager.PENDING_UPGRADE); + + Assert.equal(aAddon.pendingUpgrade.version, "2.0"); +} + +function check_addon_uninstalling(aAddon, aAfterRestart) { + Assert.notEqual(aAddon, null); + Assert.equal(aAddon.version, "1.0"); + + if (aAfterRestart) { + Assert.ok(!aAddon.isActive); + Assert.ok(!isExtensionInAddonsList(profileDir, aAddon.id)); + } else { + Assert.ok(aAddon.isActive); + Assert.ok(isExtensionInAddonsList(profileDir, aAddon.id)); + } + + Assert.ok(aAddon.hasResource("testfile")); + Assert.ok(aAddon.hasResource("testfile1")); + Assert.ok(!aAddon.hasResource("testfile2")); + + Assert.equal(aAddon.pendingOperations, AddonManager.PENDING_UNINSTALL); +} + +add_task(async function test_1() { + await AddonTestUtils.promiseInstallXPI(ADDONS[0]); + + await promiseRestartManager(); + + let a1 = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + check_addon(a1, "1.0"); + + // Lock either install.rdf for unpacked add-ons or the xpi for packed add-ons. + let uri = a1.getResourceURI("install.rdf"); + if (uri instanceof Ci.nsIJARURI) uri = uri.JARFile; + + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(uri.QueryInterface(Ci.nsIFileURL).file, -1, 0, 0); + + await AddonTestUtils.promiseInstallXPI(ADDONS[1]); + + check_addon_upgrading(a1); + + await promiseRestartManager(); + + let a1_2 = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + check_addon_upgrading(a1_2); + + await promiseRestartManager(); + + let a1_3 = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + check_addon_upgrading(a1_3); + + fstream.close(); + + await promiseRestartManager(); + + let a1_4 = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + check_addon(a1_4, "2.0"); + + await a1_4.uninstall(); +}); + +// Test that a failed uninstall gets rolled back +add_task(async function test_2() { + await promiseRestartManager(); + + await AddonTestUtils.promiseInstallXPI(ADDONS[0]); + await promiseRestartManager(); + + let a1 = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + check_addon(a1, "1.0"); + + // Lock either install.rdf for unpacked add-ons or the xpi for packed add-ons. + let uri = a1.getResourceURI("install.rdf"); + if (uri instanceof Ci.nsIJARURI) uri = uri.JARFile; + + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(uri.QueryInterface(Ci.nsIFileURL).file, -1, 0, 0); + + await a1.uninstall(); + + check_addon_uninstalling(a1); + + await promiseRestartManager(); + + let a1_2 = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + check_addon_uninstalling(a1_2, true); + + await promiseRestartManager(); + + let a1_3 = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + check_addon_uninstalling(a1_3, true); + + fstream.close(); + + await promiseRestartManager(); + + let a1_4 = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + Assert.equal(a1_4, null); + var dir = profileDir.clone(); + dir.append(do_get_expected_addon_name("addon1@tests.mozilla.org")); + Assert.ok(!dir.exists()); + Assert.ok(!isExtensionInAddonsList(profileDir, "addon1@tests.mozilla.org")); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_builtin_location.js b/toolkit/mozapps/extensions/test/xpcshell/test_builtin_location.js new file mode 100644 index 0000000000..30801e9f72 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_builtin_location.js @@ -0,0 +1,149 @@ +"use strict"; + +/* globals browser */ +let scopes = AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION; +Services.prefs.setIntPref("extensions.enabledScopes", scopes); + +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "1" +); + +async function getWrapper(id, hidden) { + let wrapper = await installBuiltinExtension({ + manifest: { + browser_specific_settings: { gecko: { id } }, + hidden, + }, + background() { + browser.test.sendMessage("started"); + }, + }); + await wrapper.awaitMessage("started"); + return wrapper; +} + +// Tests installing an extension from the built-in location. +add_task(async function test_builtin_location() { + let id = "builtin@tests.mozilla.org"; + await AddonTestUtils.promiseStartupManager(); + let wrapper = await getWrapper(id); + + let addon = await promiseAddonByID(id); + notEqual(addon, null, "Addon is installed"); + ok(addon.isActive, "Addon is active"); + ok(addon.isPrivileged, "Addon is privileged"); + ok(wrapper.extension.isAppProvided, "Addon is app provided"); + ok(!addon.hidden, "Addon is not hidden"); + + // Built-in extensions are not checked against the blocklist, + // so we shouldn't have loaded it. + ok(!Services.blocklist.isLoaded, "Blocklist hasn't been loaded"); + + // After a restart, the extension should start up normally. + await promiseRestartManager(); + await wrapper.awaitStartup(); + await wrapper.awaitMessage("started"); + ok(true, "Extension in built-in location ran after restart"); + + addon = await promiseAddonByID(id); + notEqual(addon, null, "Addon is installed"); + ok(addon.isActive, "Addon is active"); + + // After a restart that causes a database rebuild, it should still work + await promiseRestartManager("2"); + await wrapper.awaitStartup(); + await wrapper.awaitMessage("started"); + ok(true, "Extension in built-in location ran after restart"); + + addon = await promiseAddonByID(id); + notEqual(addon, null, "Addon is installed"); + ok(addon.isActive, "Addon is active"); + + // After a restart that changes the schema version, it should still work + await promiseShutdownManager(); + Services.prefs.setIntPref("extensions.databaseSchema", 0); + await promiseStartupManager(); + + await wrapper.awaitStartup(); + await wrapper.awaitMessage("started"); + ok(true, "Extension in built-in location ran after restart"); + + addon = await promiseAddonByID(id); + notEqual(addon, null, "Addon is installed"); + ok(addon.isActive, "Addon is active"); + + await wrapper.unload(); + + addon = await promiseAddonByID(id); + equal(addon, null, "Addon is gone after uninstall"); + await AddonTestUtils.promiseShutdownManager(); +}); + +// Tests installing a hidden extension from the built-in location. +add_task(async function test_builtin_location_hidden() { + let id = "hidden@tests.mozilla.org"; + await AddonTestUtils.promiseStartupManager(); + let wrapper = await getWrapper(id, true); + + let addon = await promiseAddonByID(id); + notEqual(addon, null, "Addon is installed"); + ok(addon.isActive, "Addon is active"); + ok(addon.isPrivileged, "Addon is privileged"); + ok(wrapper.extension.isAppProvided, "Addon is app provided"); + ok(addon.hidden, "Addon is hidden"); + + await wrapper.unload(); + await AddonTestUtils.promiseShutdownManager(); +}); + +// Tests updates to builtin extensions +add_task(async function test_builtin_update() { + let id = "bleah@tests.mozilla.org"; + await AddonTestUtils.promiseStartupManager(); + + let wrapper = await installBuiltinExtension({ + manifest: { + browser_specific_settings: { gecko: { id } }, + version: "1.0", + }, + background() { + browser.test.sendMessage("started"); + }, + }); + await wrapper.awaitMessage("started"); + + await AddonTestUtils.promiseShutdownManager(); + + // Change the built-in + await setupBuiltinExtension({ + manifest: { + browser_specific_settings: { gecko: { id } }, + version: "2.0", + }, + background() { + browser.test.sendMessage("started"); + }, + }); + + let updateReason; + AddonTestUtils.on("bootstrap-method", (method, params, reason) => { + updateReason = reason; + }); + + // Re-start, with a new app version + await AddonTestUtils.promiseStartupManager("3"); + + await wrapper.awaitMessage("started"); + + equal( + updateReason, + BOOTSTRAP_REASONS.ADODN_UPGRADE, + "Builtin addon's bootstrap update() method was called at startup" + ); + + await wrapper.unload(); + await AddonTestUtils.promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_cacheflush.js b/toolkit/mozapps/extensions/test/xpcshell/test_cacheflush.js new file mode 100644 index 0000000000..4a570b57fb --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_cacheflush.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies that flushing the zipreader cache happens when appropriate + +var gExpectedFile = null; +var gCacheFlushCount = 0; + +var CacheFlushObserver = { + observe(aSubject, aTopic, aData) { + if (aTopic != "flush-cache-entry") { + return; + } + + // Ignore flushes from the fake cert DB or extension-process-script + if (aData == "cert-override" || aSubject == null) { + return; + } + + if (!gExpectedFile) { + return; + } + ok(aSubject instanceof Ci.nsIFile); + equal(aSubject.path, gExpectedFile.path); + gCacheFlushCount++; + }, +}; + +add_task(async function setup() { + Services.obs.addObserver(CacheFlushObserver, "flush-cache-entry"); + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "2"); + + await promiseStartupManager(); +}); + +// Tests that the cache is flushed when installing a restartless add-on +add_task(async function test_flush_restartless_install() { + let xpi = await createTempWebExtensionFile({ + manifest: { + name: "Cache Flush Test", + version: "2.0", + browser_specific_settings: { gecko: { id: "addon2@tests.mozilla.org" } }, + }, + }); + + let install = await AddonManager.getInstallForFile(xpi); + + await new Promise(resolve => { + install.addListener({ + onInstallStarted() { + // We should flush the staged XPI when completing the install + gExpectedFile = gProfD.clone(); + gExpectedFile.append("extensions"); + gExpectedFile.append("staged"); + gExpectedFile.append("addon2@tests.mozilla.org.xpi"); + }, + + onInstallEnded() { + equal(gCacheFlushCount, 1); + gExpectedFile = null; + gCacheFlushCount = 0; + + resolve(); + }, + }); + + install.install(); + }); +}); + +// Tests that the cache is flushed when uninstalling a restartless add-on +add_task(async function test_flush_uninstall() { + let addon = await AddonManager.getAddonByID("addon2@tests.mozilla.org"); + + // We should flush the installed XPI when uninstalling + gExpectedFile = gProfD.clone(); + gExpectedFile.append("extensions"); + gExpectedFile.append("addon2@tests.mozilla.org.xpi"); + + await addon.uninstall(); + + Assert.greaterOrEqual(gCacheFlushCount, 1); + gExpectedFile = null; + gCacheFlushCount = 0; +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_childprocess.js b/toolkit/mozapps/extensions/test/xpcshell/test_childprocess.js new file mode 100644 index 0000000000..d7661d52ad --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_childprocess.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies that the AddonManager refuses to load in child processes. + +// NOTE: This test does NOT load head_addons.js, because that would indirectly +// load AddonManager.sys.mjs. In this test, we want to be the first to load the +// AddonManager module to verify that it cannot be loaded in child processes. + +const { updateAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); + +function run_test() { + updateAppInfo(); + Services.appinfo.processType = Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT; + try { + ChromeUtils.importESModule("resource://gre/modules/AddonManager.sys.mjs"); + do_throw("AddonManager should have refused to load"); + } catch (ex) { + info(ex.message); + Assert.ok(!!ex.message); + } +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_colorways_builtin_theme_upgrades.js b/toolkit/mozapps/extensions/test/xpcshell/test_colorways_builtin_theme_upgrades.js new file mode 100644 index 0000000000..154a28713d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_colorways_builtin_theme_upgrades.js @@ -0,0 +1,582 @@ +"use strict"; + +const { BuiltInThemes } = ChromeUtils.importESModule( + "resource:///modules/BuiltInThemes.sys.mjs" +); +const { BuiltInThemeConfig } = ChromeUtils.importESModule( + "resource:///modules/BuiltInThemeConfig.sys.mjs" +); + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +// Enable SCOPE_APPLICATION for builtin testing. +let scopes = AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION; +Services.prefs.setIntPref("extensions.enabledScopes", scopes); + +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +const ADDON_ID = "mock-colorway@mozilla.org"; +const ADDON_ID_RETAINED = "mock-disabled-retained-colorway@mozilla.org"; +const ADDON_ID_NOT_RETAINED = "mock-disabled-not-retained-colorway@mozilla.org"; +const DEFAULT_THEME_ID = "default-theme@mozilla.org"; +const NOT_MIGRATED_THEME = "mock-not-migrated-theme@mozilla.org"; + +const RETAINED_THEMES_PREF = "browser.theme.retainedExpiredThemes"; +const COLORWAY_MIGRATION_PREF = "browser.theme.colorway-migration"; + +const ICON_SVG = ` + <svg width="63" height="62" viewBox="0 0 63 62" fill="none" xmlns="http://www.w3.org/2000/svg"> + <circle cx="31.5" cy="31" r="31" fill="url(#paint0_linear)"/> + <defs> + <linearGradient id="paint0_linear" x1="44.4829" y1="19" x2="10.4829" y2="53" gradientUnits="userSpaceOnUse"> + <stop stop-color="hsl(147, 94%, 25%)"/> + <stop offset="1" stop-color="hsl(146, 38%, 49%)"/> + </linearGradient> + </defs> + </svg> +`; +const createMockThemeManifest = (id, version) => ({ + name: "A mock colorway theme", + author: "Mozilla", + version, + icons: { 32: "icon.svg" }, + theme: { + colors: { + toolbar: "red", + }, + }, + browser_specific_settings: { + gecko: { id }, + }, +}); + +let server = createHttpServer(); + +const SERVER_BASE_URL = `http://localhost:${server.identity.primaryPort}`; + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false); +Services.prefs.setCharPref( + "extensions.update.background.url", + `${SERVER_BASE_URL}/upgrade.json` +); + +AddonTestUtils.registerJSON(server, "/upgrade.json", { + addons: { + [ADDON_ID]: { + updates: [ + { + version: "2.0.0", + update_link: `${SERVER_BASE_URL}/${ADDON_ID}.xpi`, + }, + ], + }, + [ADDON_ID_RETAINED]: { + updates: [ + { + version: "3.0.0", + update_link: `${SERVER_BASE_URL}/${ADDON_ID_RETAINED}.xpi`, + }, + ], + }, + // We list the test extension with addon id ADDON_ID_NOT_RETAINED here, + // but the xpi file doesn't exist because we expect that we wouldn't + // be checking this extension for updates, and that expected behavior + // regresses, the test would fail either for the explicit assertion + // (checking that we don't find an update) or because we would be trying + // to download a file from an url that isn't going to be handled. + [ADDON_ID_NOT_RETAINED]: { + updates: [ + { + version: "4.0.0", + update_link: `${SERVER_BASE_URL}/non-existing.xpi`, + }, + ], + }, + }, +}); + +function createWebExtensionFile(id, version) { + return AddonTestUtils.createTempWebExtensionFile({ + files: { "icon.svg": ICON_SVG }, + manifest: createMockThemeManifest(id, version), + }); +} + +let xpiUpdate = createWebExtensionFile(ADDON_ID, "2.0.0"); +let retainedThemeUpdate = createWebExtensionFile(ADDON_ID_RETAINED, "3.0.0"); + +server.registerFile(`/${ADDON_ID}.xpi`, xpiUpdate); +server.registerFile(`/${ADDON_ID_RETAINED}.xpi`, retainedThemeUpdate); + +function assertAddonWrapperProperties( + addonWrapper, + { id, version, isBuiltin, type, isBuiltinColorwayTheme, scope } +) { + Assert.deepEqual( + { + id: addonWrapper.id, + version: addonWrapper.version, + type: addonWrapper.type, + scope: addonWrapper.scope, + isBuiltin: addonWrapper.isBuiltin, + isBuiltinColorwayTheme: addonWrapper.isBuiltinColorwayTheme, + }, + { + id, + version, + type, + scope, + isBuiltin, + isBuiltinColorwayTheme, + }, + `Got expected properties on addon wrapper for "${id}"` + ); +} + +function assertAddonCanUpgrade(addonWrapper, canUpgrade) { + equal( + !!(addonWrapper.permissions & AddonManager.PERM_CAN_UPGRADE), + canUpgrade, + `Expected "${addonWrapper.id}" to ${ + canUpgrade ? "have" : "not have" + } PERM_CAN_UPGRADE AOM permission` + ); +} + +function assertIsActiveThemeID(addonId) { + equal( + Services.prefs.getCharPref("extensions.activeThemeID"), + addonId, + `Expect ${addonId} to be the currently active theme` + ); +} + +function assertIsExpiredTheme(addonId, expectExpired) { + equal( + // themeIsExpired returns undefined for themes without an expiry date, + // normalized here to always be a boolean. + !!BuiltInThemes.themeIsExpired(addonId), + expectExpired, + `Expect ${addonId} to be recognized as an expired colorway theme` + ); +} + +function assertIsRetainedExpiredTheme(addonId, expectRetainedExpired) { + equal( + BuiltInThemes.isRetainedExpiredTheme(addonId), + expectRetainedExpired, + `Expect ${addonId} to be recognized as a retained expired colorway theme` + ); +} + +function waitForBootstrapUpdateMethod(addonId, newVersion) { + return new Promise(resolve => { + function listener(_evt, { method, params }) { + if ( + method === "update" && + params.id === addonId && + params.newVersion === newVersion + ) { + AddonTestUtils.off("bootstrap-method", listener); + info(`Update bootstrap method called for ${addonId} ${newVersion}`); + resolve(); + } + } + AddonTestUtils.on("bootstrap-method", listener); + }); +} + +let waitForTemporaryXPIFilesRemoved; + +add_setup(async () => { + info("Creating BuiltInThemes stubs"); + const sandbox = sinon.createSandbox(); + // Restoring the mocked BuiltInThemeConfig doesn't really matter for xpcshell + // because each test file will run in its own separate xpcshell instance, + // but cleaning it up doesn't harm neither. + registerCleanupFunction(() => { + info("Restoring BuiltInThemes sandbox for cleanup"); + sandbox.restore(); + BuiltInThemes.builtInThemeMap = BuiltInThemeConfig; + }); + + // Mock BuiltInThemes builtInThemeMap. + BuiltInThemes.builtInThemeMap = new Map(); + sandbox.stub(BuiltInThemes.builtInThemeMap, "get").callsFake(id => { + info(`Mock BuiltInthemes.builtInThemeMap.get result for ${id}`); + // No theme info is expected to be returned for the default-theme. + if (id === DEFAULT_THEME_ID) { + return undefined; + } + if (!id.endsWith("colorway@mozilla.org")) { + return BuiltInThemeConfig.get(id); + } + let mockThemeProperties = { + collection: "Mock expired colorway collection", + figureUrl: "about:blank", + expiry: new Date("1970-01-01"), + }; + return mockThemeProperties; + }); + + // Start AOM and make sure updates are enabled. + await AddonTestUtils.promiseStartupManager(); + AddonManager.updateEnabled = true; + + // Enable the default theme explicitly (mainly because on DevEdition builds + // the dark theme would be the one enabled by default). + const defaultTheme = await AddonManager.getAddonByID(DEFAULT_THEME_ID); + await defaultTheme.enable(); + assertIsActiveThemeID(defaultTheme.id); + + const tmpFiles = new Set(); + const addonInstallListener = { + onInstallEnded: function collectTmpFiles(install) { + tmpFiles.add(install.file); + }, + }; + AddonManager.addInstallListener(addonInstallListener); + registerCleanupFunction(() => { + AddonManager.removeInstallListener(addonInstallListener); + }); + + // Make sure all the tempfile created for the background updates have + // been removed (otherwise AddonTestUtils cleanup function will trigger + // intermittent test failures due to unexpected xpi files that may still + // be found in the temporary directory). + waitForTemporaryXPIFilesRemoved = async () => { + info( + "Wait for temporary xpi files created by the background updates to have been removed" + ); + const files = Array.from(tmpFiles); + tmpFiles.clear(); + await TestUtils.waitForCondition(async () => { + for (const file of files) { + if (await file.exists()) { + return false; + } + } + return true; + }, "Wait for the temporary files created for the background updates to have been removed"); + }; +}); + +add_task( + { + pref_set: [[COLORWAY_MIGRATION_PREF, false]], + }, + async function test_colorways_migration_disabled() { + info("Install and activate a colorway built-in test theme"); + + await installBuiltinExtension( + { + manifest: createMockThemeManifest(ADDON_ID, "1.0.0"), + }, + false /* waitForStartup */ + ); + const activeTheme = await AddonManager.getAddonByID(ADDON_ID); + assertAddonWrapperProperties(activeTheme, { + id: ADDON_ID, + version: "1.0.0", + type: "theme", + scope: AddonManager.SCOPE_APPLICATION, + isBuiltin: true, + isBuiltinColorwayTheme: true, + }); + const promiseThemeEnabled = AddonTestUtils.promiseAddonEvent( + "onEnabled", + addon => addon.id === ADDON_ID + ); + await activeTheme.enable(); + await promiseThemeEnabled; + ok(activeTheme.isActive, "Expect the colorways theme to be active"); + assertIsActiveThemeID(activeTheme.id); + + info("Verify that built-in colorway migration is disabled as expected"); + + assertAddonCanUpgrade(activeTheme, false); + + const promiseBackgroundUpdatesFound = TestUtils.topicObserved( + "addons-background-updates-found" + ); + await AddonManagerPrivate.backgroundUpdateCheck(); + const [, numUpdatesFound] = await promiseBackgroundUpdatesFound; + equal(numUpdatesFound, 0, "Expect no add-on updates to be found"); + + await activeTheme.uninstall(); + } +); + +add_task( + { + pref_set: [ + [COLORWAY_MIGRATION_PREF, true], + [ + RETAINED_THEMES_PREF, + JSON.stringify([ADDON_ID_RETAINED, NOT_MIGRATED_THEME]), + ], + ], + }, + async function test_colorways_builtin_upgrade() { + info("Verify default theme initially enabled"); + const defaultTheme = await AddonManager.getAddonByID(DEFAULT_THEME_ID); + assertAddonWrapperProperties(defaultTheme, { + id: DEFAULT_THEME_ID, + version: defaultTheme.version, + type: "theme", + scope: AddonManager.SCOPE_APPLICATION, + isBuiltin: true, + isBuiltinColorwayTheme: false, + }); + ok( + defaultTheme.isActive, + "Expect the default theme to be initially active" + ); + assertIsActiveThemeID(defaultTheme.id); + + info("Install the non retained expired colorway test theme"); + await installBuiltinExtension( + { + manifest: createMockThemeManifest(ADDON_ID_NOT_RETAINED, "1.0.0"), + }, + false /* waitForStartup */ + ); + const notRetainedTheme = await AddonManager.getAddonByID( + ADDON_ID_NOT_RETAINED + ); + assertAddonWrapperProperties(notRetainedTheme, { + id: ADDON_ID_NOT_RETAINED, + version: "1.0.0", + type: "theme", + scope: AddonManager.SCOPE_APPLICATION, + isBuiltin: true, + isBuiltinColorwayTheme: true, + }); + + info("Install the retained expired colorway test theme"); + await installBuiltinExtension( + { + manifest: createMockThemeManifest(ADDON_ID_RETAINED, "1.0.0"), + }, + false /* waitForStartup */ + ); + const retainedTheme = await AddonManager.getAddonByID(ADDON_ID_RETAINED); + assertAddonWrapperProperties(retainedTheme, { + id: ADDON_ID_RETAINED, + version: "1.0.0", + type: "theme", + scope: AddonManager.SCOPE_APPLICATION, + isBuiltin: true, + isBuiltinColorwayTheme: true, + }); + + info("Install the active colorway test theme"); + await installBuiltinExtension( + { + manifest: createMockThemeManifest(ADDON_ID, "1.0.0"), + }, + false /* waitForStartup */ + ); + const activeTheme = await AddonManager.getAddonByID(ADDON_ID); + assertAddonWrapperProperties(activeTheme, { + id: ADDON_ID, + version: "1.0.0", + type: "theme", + scope: AddonManager.SCOPE_APPLICATION, + isBuiltin: true, + isBuiltinColorwayTheme: true, + }); + const promiseThemeEnabled = AddonTestUtils.promiseAddonEvent( + "onEnabled", + addon => addon.id === ADDON_ID + ); + await activeTheme.enable(); + await promiseThemeEnabled; + ok(activeTheme.isActive, "Expect the colorways theme to be active"); + assertIsActiveThemeID(activeTheme.id); + + info("Verify only active and retained colorways themes can be upgraded"); + assertIsActiveThemeID(activeTheme.id); + assertIsExpiredTheme(activeTheme.id, true); + assertIsRetainedExpiredTheme(activeTheme.id, false); + + assertIsExpiredTheme(retainedTheme.id, true); + assertIsRetainedExpiredTheme(retainedTheme.id, true); + + assertIsExpiredTheme(notRetainedTheme.id, true); + assertIsRetainedExpiredTheme(notRetainedTheme.id, false); + + assertIsExpiredTheme(defaultTheme.id, false); + assertIsRetainedExpiredTheme(defaultTheme.id, false); + + assertAddonCanUpgrade(retainedTheme, true); + assertAddonCanUpgrade(notRetainedTheme, false); + assertAddonCanUpgrade(activeTheme, true); + // Make sure a non-colorways built-in theme cannot check for updates. + assertAddonCanUpgrade(defaultTheme, false); + + Assert.deepEqual( + Services.prefs.getStringPref(RETAINED_THEMES_PREF), + JSON.stringify([retainedTheme.id, NOT_MIGRATED_THEME]), + `Expect the retained theme id to be listed in the ${RETAINED_THEMES_PREF} pref` + ); + + const promiseUpdatesInstalled = Promise.all([ + waitForBootstrapUpdateMethod(ADDON_ID, "2.0.0"), + waitForBootstrapUpdateMethod(ADDON_ID_RETAINED, "3.0.0"), + ]); + + const promiseInstallsEnded = Promise.all([ + AddonTestUtils.promiseInstallEvent( + "onInstallEnded", + install => install.addon.id === ADDON_ID + ), + AddonTestUtils.promiseInstallEvent( + "onInstallEnded", + install => install.addon.id === ADDON_ID_RETAINED + ), + ]); + + const promiseActiveThemeStartupCompleted = + AddonTestUtils.promiseWebExtensionStartup(ADDON_ID); + + const promiseBackgroundUpdatesFound = TestUtils.topicObserved( + "addons-background-updates-found" + ); + await AddonManagerPrivate.backgroundUpdateCheck(); + const [, numUpdatesFound] = await promiseBackgroundUpdatesFound; + equal(numUpdatesFound, 2, "Expect 2 add-on updates to have been found"); + + info("Wait for the 2 expected updates to be completed"); + await promiseUpdatesInstalled; + + const updatedActiveTheme = await AddonManager.getAddonByID(ADDON_ID); + assertAddonWrapperProperties(updatedActiveTheme, { + id: ADDON_ID, + version: "2.0.0", + type: "theme", + scope: AddonManager.SCOPE_PROFILE, + isBuiltin: false, + isBuiltinColorwayTheme: false, + }); + // Expect the updated active theme to stay set as the currently active theme. + assertIsActiveThemeID(updatedActiveTheme.id); + + info("Verify addon update on disabled builtin colorway theme"); + + const updatedRetainedTheme = await AddonManager.getAddonByID( + ADDON_ID_RETAINED + ); + assertAddonWrapperProperties(updatedRetainedTheme, { + id: ADDON_ID_RETAINED, + version: "3.0.0", + type: "theme", + scope: AddonManager.SCOPE_PROFILE, + isBuiltin: false, + isBuiltinColorwayTheme: false, + }); + // Expect the updated active theme to stay set as the currently active theme. + assertIsActiveThemeID(updatedActiveTheme.id); + ok(updatedActiveTheme.isActive, "Expect the colorways theme to be active"); + + // We need to wait for the active theme startup otherwise uninstall the active + // theme will fail to remove the xpi file because it is stil active while the + // test is running on windows builds. + info("Wait for the active theme to have been fully loaded"); + await promiseActiveThemeStartupCompleted; + + await promiseInstallsEnded; + + Assert.deepEqual( + Services.prefs.getStringPref(RETAINED_THEMES_PREF), + JSON.stringify([NOT_MIGRATED_THEME]), + `Expect migrated retained theme to not be listed anymore in the ${RETAINED_THEMES_PREF} pref` + ); + + info( + "uninstall test colorways themes and expect default theme to become active" + ); + + const promiseUninstalled = Promise.all([ + AddonTestUtils.promiseAddonEvent( + "onUninstalled", + addon => addon.id === ADDON_ID + ), + AddonTestUtils.promiseAddonEvent( + "onUninstalled", + addon => addon.id === ADDON_ID_RETAINED + ), + AddonTestUtils.promiseAddonEvent( + "onUninstalled", + addon => addon.id === ADDON_ID_NOT_RETAINED + ), + ]); + + const promiseDefaultThemeEnabled = + AddonTestUtils.promiseAddonEvent("onEnabled"); + + await updatedActiveTheme.uninstall(); + await updatedRetainedTheme.uninstall(); + await notRetainedTheme.uninstall(); + + await promiseUninstalled; + + info("Wait for the default theme to become active"); + // Waiting explicitly for the onEnabled addon event prevents a race between + // the test task exiting (and the AddonManager being shutdown automatically + // as a side effect of that) and the XPIProvider trying to call the addon event + // listeners for the default theme being enabled), which would trigger a test + // failure after the test is existing. + await promiseDefaultThemeEnabled; + + ok(defaultTheme.isActive, "Expect the default theme to be active"); + assertIsActiveThemeID(defaultTheme.id); + + // Wait for the temporary file to be actually removed, otherwise the hack we use + // to mock an AOM restart (which is unloading the related jsm modules) may + // affect the successfull removal of the temporary file because some of the + // global helpers defined and used inside the XPIProvider may have been gone + // already and intermittently trigger unexpected errors. + await waitForTemporaryXPIFilesRemoved(); + + // Restart the addon manager to confirm that the migrated colorways themes + // are still gone after an AOM restart and that the previously installed + // builtin hasn't been made implicitly visible again. + info( + "Verify old builtin colorways are not visible and default-theme still active after AOM restart" + ); + await AddonTestUtils.promiseRestartManager(); + + const defaultThemeAfterRestart = await AddonManager.getAddonByID( + DEFAULT_THEME_ID + ); + ok( + defaultThemeAfterRestart.isActive, + "Expect the default theme to be active" + ); + + equal( + (await AddonManager.getAddonByID(ADDON_ID))?.version, + undefined, + "Expect the active theme addon to not be available anymore after being uninstalled" + ); + equal( + (await AddonManager.getAddonByID(ADDON_ID_RETAINED))?.version, + undefined, + "Expect the retained theme addon to not be available anymore after being uninstalled" + ); + equal( + (await AddonManager.getAddonByID(ADDON_ID_NOT_RETAINED))?.version, + undefined, + "Expect the not retained expired theme addon to not be available anymore after being uninstalled" + ); + } +); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_cookies.js b/toolkit/mozapps/extensions/test/xpcshell/test_cookies.js new file mode 100644 index 0000000000..56f745929b --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_cookies.js @@ -0,0 +1,102 @@ +"use strict"; + +let server = createHttpServer({ hosts: ["example.com"] }); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "45", "45"); + +Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", true); +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); + +// Tests that cookies are not sent with background requests. +add_task(async function test_cookies() { + const ID = "bg-cookies@tests.mozilla.org"; + + // Add a new handler to the test web server for the given file path. + // The handler appends the incoming requests to `results` and replies + // with the provided body. + function makeHandler(path, results, body) { + server.registerPathHandler(path, (request, response) => { + results.push(request); + response.write(body); + }); + } + + let gets = []; + makeHandler("/get", gets, JSON.stringify({ results: [] })); + Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, "http://example.com/get"); + + let updates = []; + makeHandler( + "/update", + updates, + JSON.stringify({ + addons: { + [ID]: { + updates: [ + { + version: "2.0", + update_link: "http://example.com/update.xpi", + applications: { + gecko: {}, + }, + }, + ], + }, + }, + }) + ); + + let xpiFetches = []; + makeHandler("/update.xpi", xpiFetches, ""); + + const COOKIE = "test"; + // cookies.add() takes a time in seconds + let expiration = Date.now() / 1000 + 60 * 60; + Services.cookies.add( + "example.com", + "/", + COOKIE, + "testing", + false, + false, + false, + expiration, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTP + ); + + await promiseStartupManager(); + + let addon = await promiseInstallWebExtension({ + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: ID, + update_url: "http://example.com/update", + }, + }, + }, + }); + + equal(gets.length, 1, "Saw one addon metadata request"); + equal(gets[0].hasHeader("Cookie"), false, "Metadata request has no cookies"); + + await Promise.all([ + AddonTestUtils.promiseInstallEvent("onDownloadFailed"), + AddonManagerPrivate.backgroundUpdateCheck(), + ]); + + equal(updates.length, 1, "Saw one update check request"); + equal(updates[0].hasHeader("Cookie"), false, "Update request has no cookies"); + + equal(xpiFetches.length, 1, "Saw one request for updated xpi"); + equal( + xpiFetches[0].hasHeader("Cookie"), + false, + "Request for updated XPI has no cookies" + ); + + await addon.uninstall(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_corrupt.js b/toolkit/mozapps/extensions/test/xpcshell/test_corrupt.js new file mode 100644 index 0000000000..727c643763 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_corrupt.js @@ -0,0 +1,216 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Checks that we rebuild something sensible from a corrupt database + +// Create and configure the HTTP server. +var testserver = createHttpServer({ hosts: ["example.com"] }); + +// register files with server +testserver.registerDirectory("/data/", do_get_file("data")); + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); + +const ADDONS = { + // Will get a compatibility update and stay enabled + "addon3@tests.mozilla.org": { + manifest: { + name: "Test 3", + browser_specific_settings: { + gecko: { + id: "addon3@tests.mozilla.org", + update_url: "http://example.com/data/test_corrupt.json", + }, + }, + }, + findUpdates: true, + desiredState: { + isActive: true, + userDisabled: false, + appDisabled: false, + pendingOperations: 0, + }, + }, + + // Will get a compatibility update and be enabled + "addon4@tests.mozilla.org": { + manifest: { + name: "Test 4", + browser_specific_settings: { + gecko: { + id: "addon4@tests.mozilla.org", + update_url: "http://example.com/data/test_corrupt.json", + }, + }, + }, + initialState: { + userDisabled: true, + }, + findUpdates: true, + desiredState: { + isActive: false, + userDisabled: true, + appDisabled: false, + pendingOperations: 0, + }, + }, + + "addon5@tests.mozilla.org": { + manifest: { + name: "Test 5", + browser_specific_settings: { gecko: { id: "addon5@tests.mozilla.org" } }, + }, + desiredState: { + isActive: true, + userDisabled: false, + appDisabled: false, + pendingOperations: 0, + }, + }, + + "addon7@tests.mozilla.org": { + manifest: { + name: "Test 7", + browser_specific_settings: { gecko: { id: "addon7@tests.mozilla.org" } }, + }, + initialState: { + userDisabled: true, + }, + desiredState: { + isActive: false, + userDisabled: true, + appDisabled: false, + pendingOperations: 0, + }, + }, + + // The default theme + "theme1@tests.mozilla.org": { + manifest: { + manifest_version: 2, + name: "Theme 1", + version: "1.0", + theme: { images: { theme_frame: "example.png" } }, + browser_specific_settings: { + gecko: { + id: "theme1@tests.mozilla.org", + }, + }, + }, + desiredState: { + isActive: false, + userDisabled: true, + appDisabled: false, + pendingOperations: 0, + }, + }, + + "theme2@tests.mozilla.org": { + manifest: { + manifest_version: 2, + name: "Theme 2", + version: "1.0", + theme: { images: { theme_frame: "example.png" } }, + browser_specific_settings: { + gecko: { + id: "theme2@tests.mozilla.org", + }, + }, + }, + initialState: { + userDisabled: false, + }, + desiredState: { + isActive: true, + userDisabled: false, + appDisabled: false, + pendingOperations: 0, + }, + }, +}; + +const IDS = Object.keys(ADDONS); + +function promiseUpdates(addon) { + return new Promise(resolve => { + addon.findUpdates( + { onUpdateFinished: resolve }, + AddonManager.UPDATE_WHEN_PERIODIC_UPDATE + ); + }); +} + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2"); + + for (let addon of Object.values(ADDONS)) { + let webext = createTempWebExtensionFile({ manifest: addon.manifest }); + await AddonTestUtils.manuallyInstall(webext); + } + + await promiseStartupManager(); + + let addons = await getAddons(IDS); + for (let [id, addon] of Object.entries(ADDONS)) { + if (addon.initialState) { + await setInitialState(addons.get(id), addon.initialState); + } + if (addon.findUpdates) { + await promiseUpdates(addons.get(id)); + } + } +}); + +add_task(async function test_after_restart() { + await promiseRestartManager(); + + info("Test add-on state after restart"); + let addons = await getAddons(IDS); + for (let [id, addon] of Object.entries(ADDONS)) { + checkAddon(id, addons.get(id), addon.desiredState); + } + + await promiseShutdownManager(); +}); + +add_task(async function test_after_corruption() { + // Shutdown and replace the database with a corrupt file (a directory + // serves this purpose). On startup the add-ons manager won't rebuild + // because there is a file there still. + gExtensionsJSON.remove(true); + gExtensionsJSON.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + + await promiseStartupManager(); + + Services.obs.notifyObservers(null, "sessionstore-windows-restored"); + await AddonManagerPrivate.databaseReady; + + // Accessing the add-ons should open and recover the database + info("Test add-on state after corruption"); + let addons = await getAddons(IDS); + for (let [id, addon] of Object.entries(ADDONS)) { + checkAddon(id, addons.get(id), addon.desiredState); + } + + await Assert.rejects( + promiseShutdownManager(), + /NotAllowedError: Could not open the file at .+ for writing/ + ); +}); + +add_task(async function test_after_second_restart() { + await promiseStartupManager(); + + info("Test add-on state after second restart"); + let addons = await getAddons(IDS); + for (let [id, addon] of Object.entries(ADDONS)) { + checkAddon(id, addons.get(id), addon.desiredState); + } + + await Assert.rejects( + promiseShutdownManager(), + /NotAllowedError: Could not open the file at .+ for writing/ + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_crash_annotation_quoting.js b/toolkit/mozapps/extensions/test/xpcshell/test_crash_annotation_quoting.js new file mode 100644 index 0000000000..4458c6d592 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_crash_annotation_quoting.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies that strange characters in an add-on version don't break the +// crash annotation. + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + +add_task(async function run_test() { + await promiseStartupManager(); + + let n = 1; + for (let version in ["1,0", "1:0"]) { + let id = `addon${n++}@tests.mozilla.org`; + await promiseInstallWebExtension({ + manifest: { + version, + browser_specific_settings: { gecko: { id } }, + }, + }); + + do_check_in_crash_annotation(id, version); + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_db_path.js b/toolkit/mozapps/extensions/test/xpcshell/test_db_path.js new file mode 100644 index 0000000000..a9a54291f0 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_db_path.js @@ -0,0 +1,64 @@ +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const DEFAULT_THEME_ID = "default-theme@mozilla.org"; + +let global = this; + +// Test that paths in the extensions database are stored properly +// if they include non-ascii characters (see bug 1428234 for an example of +// a past bug with such paths) +add_task(async function test_non_ascii_path() { + const PROFILE_VAR = "XPCSHELL_TEST_PROFILE_DIR"; + let profileDir = PathUtils.join( + Services.env.get(PROFILE_VAR), + "\u00ce \u00e5m \u00f1\u00f8t \u00e5s\u00e7ii" + ); + Services.env.set(PROFILE_VAR, profileDir); + + AddonTestUtils.init(global); + AddonTestUtils.overrideCertDB(); + AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "1" + ); + + const ID1 = "profile1@tests.mozilla.org"; + let xpi1 = await AddonTestUtils.createTempWebExtensionFile({ + id: ID1, + manifest: { + browser_specific_settings: { gecko: { id: ID1 } }, + }, + }); + + const ID2 = "profile2@tests.mozilla.org"; + let xpi2 = await AddonTestUtils.createTempWebExtensionFile({ + id: ID2, + manifest: { + browser_specific_settings: { gecko: { id: ID2 } }, + }, + }); + + await AddonTestUtils.manuallyInstall(xpi1); + await AddonTestUtils.promiseStartupManager(); + await AddonTestUtils.promiseInstallFile(xpi2); + await AddonTestUtils.promiseShutdownManager(); + + let dbfile = PathUtils.join(profileDir, "extensions.json"); + let data = await IOUtils.readJSON(dbfile); + + let addons = data.addons.filter(a => a.id !== DEFAULT_THEME_ID); + Assert.ok(Array.isArray(addons), "extensions.json has addons array"); + Assert.equal(2, addons.length, "extensions.json has 2 addons"); + Assert.ok( + addons[0].path.startsWith(profileDir), + "path property for sideloaded extension has the proper profile directory" + ); + Assert.ok( + addons[1].path.startsWith(profileDir), + "path property for extension installed at runtime has the proper profile directory" + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js b/toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js new file mode 100644 index 0000000000..7b1c6fbef9 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js @@ -0,0 +1,556 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies that delaying an update works for WebExtensions. + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); + +if (AppConstants.platform == "win" && AppConstants.DEBUG) { + // Shutdown timing is flaky in this test, and remote extensions + // sometimes wind up leaving the XPI locked at the point when we try + // to remove it. + Services.prefs.setBoolPref("extensions.webextensions.remote", false); +} + +PromiseTestUtils.allowMatchingRejectionsGlobally( + /Message manager disconnected/ +); + +/* globals browser*/ + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); +const stageDir = profileDir.clone(); +stageDir.append("staged"); + +const IGNORE_ID = "test_delay_update_ignore_webext@tests.mozilla.org"; +const COMPLETE_ID = "test_delay_update_complete_webext@tests.mozilla.org"; +const DEFER_ID = "test_delay_update_defer_webext@tests.mozilla.org"; +const STAGED_ID = "test_delay_update_staged_webext@tests.mozilla.org"; +const STAGED_NO_UPDATE_URL_ID = + "test_delay_update_staged_webext_no_update_url@tests.mozilla.org"; +const NOUPDATE_ID = "test_no_update_webext@tests.mozilla.org"; + +// Create and configure the HTTP server. +var testserver = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); +testserver.registerDirectory("/data/", do_get_file("data")); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42", "42"); +BootstrapMonitor.init(); + +const ADDONS = { + test_delay_update_complete_webextension_v2: { + "manifest.json": { + manifest_version: 2, + name: "Delay Upgrade", + version: "2.0", + browser_specific_settings: { + gecko: { id: COMPLETE_ID }, + }, + }, + }, + test_delay_update_defer_webextension_v2: { + "manifest.json": { + manifest_version: 2, + name: "Delay Upgrade", + version: "2.0", + browser_specific_settings: { + gecko: { id: DEFER_ID }, + }, + }, + }, + test_delay_update_staged_webextension_v2: { + "manifest.json": { + manifest_version: 2, + name: "Delay Upgrade", + version: "2.0", + browser_specific_settings: { + gecko: { + id: STAGED_ID, + update_url: `http://example.com/data/test_delay_updates_staged.json`, + strict_min_version: "1", + strict_max_version: "41", + }, + }, + }, + }, + test_delay_update_staged_webextension_no_update_url_v2: { + "manifest.json": { + manifest_version: 2, + name: "Delay Upgrade", + version: "2.0", + browser_specific_settings: { + gecko: { + id: STAGED_NO_UPDATE_URL_ID, + strict_min_version: "1", + strict_max_version: "41", + }, + }, + }, + }, + test_delay_update_ignore_webextension_v2: { + "manifest.json": { + manifest_version: 2, + name: "Delay Upgrade", + version: "2.0", + browser_specific_settings: { + gecko: { id: IGNORE_ID }, + }, + }, + }, +}; + +const XPIS = {}; +for (let [name, files] of Object.entries(ADDONS)) { + XPIS[name] = AddonTestUtils.createTempXPIFile(files); + testserver.registerFile(`/addons/${name}.xpi`, XPIS[name]); +} + +// add-on registers upgrade listener, and ignores update. +add_task(async function delay_updates_ignore() { + await promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: IGNORE_ID, + update_url: `http://example.com/data/test_delay_updates_ignore.json`, + }, + }, + }, + background() { + browser.runtime.onUpdateAvailable.addListener(details => { + if (details) { + if (details.version) { + // This should be the version of the pending update. + browser.test.assertEq("2.0", details.version, "correct version"); + browser.test.notifyPass("delay"); + } + } else { + browser.test.fail("no details object passed"); + } + }); + browser.test.sendMessage("ready"); + }, + }); + + await Promise.all([extension.startup(), extension.awaitMessage("ready")]); + BootstrapMonitor.checkInstalled(IGNORE_ID, "1.0"); + + let addon = await promiseAddonByID(IGNORE_ID); + Assert.notEqual(addon, null); + Assert.equal(addon.version, "1.0"); + Assert.equal(addon.name, "Generated extension"); + Assert.ok(addon.isCompatible); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.type, "extension"); + + let update = await promiseFindAddonUpdates(addon); + let install = update.updateAvailable; + + await promiseCompleteAllInstalls([install]); + + Assert.equal(install.state, AddonManager.STATE_POSTPONED); + BootstrapMonitor.checkInstalled(IGNORE_ID, "1.0"); + + // addon upgrade has been delayed + let addon_postponed = await promiseAddonByID(IGNORE_ID); + Assert.notEqual(addon_postponed, null); + Assert.equal(addon_postponed.version, "1.0"); + Assert.equal(addon_postponed.name, "Generated extension"); + Assert.ok(addon_postponed.isCompatible); + Assert.ok(!addon_postponed.appDisabled); + Assert.ok(addon_postponed.isActive); + Assert.equal(addon_postponed.type, "extension"); + + await extension.awaitFinish("delay"); + + // restarting allows upgrade to proceed + await promiseRestartManager(); + + let addon_upgraded = await promiseAddonByID(IGNORE_ID); + await extension.awaitStartup(); + BootstrapMonitor.checkUpdated(IGNORE_ID, "2.0"); + + Assert.notEqual(addon_upgraded, null); + Assert.equal(addon_upgraded.version, "2.0"); + Assert.equal(addon_upgraded.name, "Delay Upgrade"); + Assert.ok(addon_upgraded.isCompatible); + Assert.ok(!addon_upgraded.appDisabled); + Assert.ok(addon_upgraded.isActive); + Assert.equal(addon_upgraded.type, "extension"); + + await extension.unload(); + await promiseShutdownManager(); +}); + +// add-on registers upgrade listener, and allows update. +add_task(async function delay_updates_complete() { + await promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: COMPLETE_ID, + update_url: `http://example.com/data/test_delay_updates_complete.json`, + }, + }, + }, + background() { + browser.runtime.onUpdateAvailable.addListener(details => { + browser.test.notifyPass("reload"); + browser.runtime.reload(); + }); + browser.test.sendMessage("ready"); + }, + }); + + await Promise.all([extension.startup(), extension.awaitMessage("ready")]); + + let addon = await promiseAddonByID(COMPLETE_ID); + Assert.notEqual(addon, null); + Assert.equal(addon.version, "1.0"); + Assert.equal(addon.name, "Generated extension"); + Assert.ok(addon.isCompatible); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.type, "extension"); + + let update = await promiseFindAddonUpdates(addon); + let install = update.updateAvailable; + + let promiseInstalled = promiseAddonEvent("onInstalled"); + await promiseCompleteAllInstalls([install]); + + await extension.awaitFinish("reload"); + + // addon upgrade has been allowed + let [addon_allowed] = await promiseInstalled; + await extension.awaitStartup(); + + Assert.notEqual(addon_allowed, null); + Assert.equal(addon_allowed.version, "2.0"); + Assert.equal(addon_allowed.name, "Delay Upgrade"); + Assert.ok(addon_allowed.isCompatible); + Assert.ok(!addon_allowed.appDisabled); + Assert.ok(addon_allowed.isActive); + Assert.equal(addon_allowed.type, "extension"); + + await new Promise(executeSoon); + + if (stageDir.exists()) { + do_throw( + "Staging directory should not exist for formerly-postponed extension" + ); + } + + await extension.unload(); + await promiseShutdownManager(); +}); + +// add-on registers upgrade listener, initially defers update then allows upgrade +add_task(async function delay_updates_defer() { + await promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: DEFER_ID, + update_url: `http://example.com/data/test_delay_updates_defer.json`, + }, + }, + }, + background() { + browser.runtime.onUpdateAvailable.addListener(details => { + // Upgrade will only proceed when "allow" message received. + browser.test.onMessage.addListener(msg => { + if (msg == "allow") { + browser.test.notifyPass("allowed"); + browser.runtime.reload(); + } else { + browser.test.fail(`wrong message: ${msg}`); + } + }); + browser.test.sendMessage("truly ready"); + }); + browser.test.sendMessage("ready"); + }, + }); + + await Promise.all([extension.startup(), extension.awaitMessage("ready")]); + + let addon = await promiseAddonByID(DEFER_ID); + Assert.notEqual(addon, null); + Assert.equal(addon.version, "1.0"); + Assert.equal(addon.name, "Generated extension"); + Assert.ok(addon.isCompatible); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.type, "extension"); + + let update = await promiseFindAddonUpdates(addon); + let install = update.updateAvailable; + + let promiseInstalled = promiseAddonEvent("onInstalled"); + await promiseCompleteAllInstalls([install]); + + Assert.equal(install.state, AddonManager.STATE_POSTPONED); + + // upgrade is initially postponed + let addon_postponed = await promiseAddonByID(DEFER_ID); + Assert.notEqual(addon_postponed, null); + Assert.equal(addon_postponed.version, "1.0"); + Assert.equal(addon_postponed.name, "Generated extension"); + Assert.ok(addon_postponed.isCompatible); + Assert.ok(!addon_postponed.appDisabled); + Assert.ok(addon_postponed.isActive); + Assert.equal(addon_postponed.type, "extension"); + + // add-on will not allow upgrade until message is received + await extension.awaitMessage("truly ready"); + extension.sendMessage("allow"); + await extension.awaitFinish("allowed"); + + // addon upgrade has been allowed + let [addon_allowed] = await promiseInstalled; + await extension.awaitStartup(); + + Assert.notEqual(addon_allowed, null); + Assert.equal(addon_allowed.version, "2.0"); + Assert.equal(addon_allowed.name, "Delay Upgrade"); + Assert.ok(addon_allowed.isCompatible); + Assert.ok(!addon_allowed.appDisabled); + Assert.ok(addon_allowed.isActive); + Assert.equal(addon_allowed.type, "extension"); + + await promiseRestartManager(); + + // restart changes nothing + addon_allowed = await promiseAddonByID(DEFER_ID); + await extension.awaitStartup(); + + Assert.notEqual(addon_allowed, null); + Assert.equal(addon_allowed.version, "2.0"); + Assert.equal(addon_allowed.name, "Delay Upgrade"); + Assert.ok(addon_allowed.isCompatible); + Assert.ok(!addon_allowed.appDisabled); + Assert.ok(addon_allowed.isActive); + Assert.equal(addon_allowed.type, "extension"); + + await extension.unload(); + await promiseShutdownManager(); +}); + +// add-on registers upgrade listener to deny update, completes after restart, +// even though the updated XPI is incompatible - the information returned +// by the update server defined in its manifest returns a compatible range +add_task(async function delay_updates_staged() { + await promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: STAGED_ID, + update_url: `http://example.com/data/test_delay_updates_staged.json`, + }, + }, + }, + background() { + browser.runtime.onUpdateAvailable.addListener(details => { + browser.test.sendMessage("denied"); + }); + browser.test.sendMessage("ready"); + }, + }); + + await Promise.all([extension.startup(), extension.awaitMessage("ready")]); + + let addon = await promiseAddonByID(STAGED_ID); + Assert.notEqual(addon, null); + Assert.equal(addon.version, "1.0"); + Assert.equal(addon.name, "Generated extension"); + Assert.ok(addon.isCompatible); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.type, "extension"); + + let update = await promiseFindAddonUpdates(addon); + let install = update.updateAvailable; + await promiseCompleteAllInstalls([install]); + + Assert.equal(install.state, AddonManager.STATE_POSTPONED); + + // upgrade is initially postponed + let addon_postponed = await promiseAddonByID(STAGED_ID); + Assert.notEqual(addon_postponed, null); + Assert.equal(addon_postponed.version, "1.0"); + Assert.equal(addon_postponed.name, "Generated extension"); + Assert.ok(addon_postponed.isCompatible); + Assert.ok(!addon_postponed.appDisabled); + Assert.ok(addon_postponed.isActive); + Assert.equal(addon_postponed.type, "extension"); + + // add-on reports an available upgrade, but denied it till next restart + await extension.awaitMessage("denied"); + + await promiseRestartManager(); + await extension.awaitStartup(); + + // add-on should have been updated during restart + let addon_upgraded = await promiseAddonByID(STAGED_ID); + Assert.notEqual(addon_upgraded, null); + Assert.equal(addon_upgraded.version, "2.0"); + Assert.equal(addon_upgraded.name, "Delay Upgrade"); + Assert.ok(addon_upgraded.isCompatible); + Assert.ok(!addon_upgraded.appDisabled); + Assert.ok(addon_upgraded.isActive); + Assert.equal(addon_upgraded.type, "extension"); + + await extension.unload(); + await promiseShutdownManager(); +}); + +// add-on registers upgrade listener to deny update, does not complete after +// restart, because the updated XPI is incompatible - there is no update server +// defined in its manifest, which could return a compatible range +add_task(async function delay_updates_staged_no_update_url() { + await promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: STAGED_NO_UPDATE_URL_ID, + update_url: `http://example.com/data/test_delay_updates_staged.json`, + }, + }, + }, + background() { + browser.runtime.onUpdateAvailable.addListener(details => { + browser.test.sendMessage("denied"); + }); + browser.test.sendMessage("ready"); + }, + }); + + await Promise.all([extension.startup(), extension.awaitMessage("ready")]); + + let addon = await promiseAddonByID(STAGED_NO_UPDATE_URL_ID); + Assert.notEqual(addon, null); + Assert.equal(addon.version, "1.0"); + Assert.equal(addon.name, "Generated extension"); + Assert.ok(addon.isCompatible); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.type, "extension"); + + let update = await promiseFindAddonUpdates(addon); + let install = update.updateAvailable; + await promiseCompleteAllInstalls([install]); + + Assert.equal(install.state, AddonManager.STATE_POSTPONED); + + // upgrade is initially postponed + let addon_postponed = await promiseAddonByID(STAGED_NO_UPDATE_URL_ID); + Assert.notEqual(addon_postponed, null); + Assert.equal(addon_postponed.version, "1.0"); + Assert.equal(addon_postponed.name, "Generated extension"); + Assert.ok(addon_postponed.isCompatible); + Assert.ok(!addon_postponed.appDisabled); + Assert.ok(addon_postponed.isActive); + Assert.equal(addon_postponed.type, "extension"); + + // add-on reports an available upgrade, but denied it till next restart + await extension.awaitMessage("denied"); + + await promiseRestartManager(); + await extension.awaitStartup(); + + // add-on should not have been updated during restart + let addon_upgraded = await promiseAddonByID(STAGED_NO_UPDATE_URL_ID); + Assert.notEqual(addon_upgraded, null); + Assert.equal(addon_upgraded.version, "1.0"); + Assert.equal(addon_upgraded.name, "Generated extension"); + Assert.ok(addon_upgraded.isCompatible); + Assert.ok(!addon_upgraded.appDisabled); + Assert.ok(addon_upgraded.isActive); + Assert.equal(addon_upgraded.type, "extension"); + + await extension.unload(); + await promiseShutdownManager(); +}); + +// browser.runtime.reload() without a pending upgrade should just reload. +add_task(async function runtime_reload() { + await promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: NOUPDATE_ID, + update_url: `http://example.com/data/test_no_update.json`, + }, + }, + }, + background() { + browser.test.onMessage.addListener(msg => { + if (msg == "reload") { + browser.runtime.reload(); + } else { + browser.test.fail(`wrong message: ${msg}`); + } + }); + browser.test.sendMessage("ready"); + }, + }); + + await Promise.all([extension.startup(), extension.awaitMessage("ready")]); + + let addon = await promiseAddonByID(NOUPDATE_ID); + Assert.notEqual(addon, null); + Assert.equal(addon.version, "1.0"); + Assert.equal(addon.name, "Generated extension"); + Assert.ok(addon.isCompatible); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.type, "extension"); + + await promiseFindAddonUpdates(addon); + + extension.sendMessage("reload"); + // Wait for extension to restart, to make sure reload works. + await AddonTestUtils.promiseWebExtensionStartup(NOUPDATE_ID); + await extension.awaitMessage("ready"); + + addon = await promiseAddonByID(NOUPDATE_ID); + Assert.notEqual(addon, null); + Assert.equal(addon.version, "1.0"); + Assert.equal(addon.name, "Generated extension"); + Assert.ok(addon.isCompatible); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.type, "extension"); + + await extension.unload(); + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_dependencies.js b/toolkit/mozapps/extensions/test/xpcshell/test_dependencies.js new file mode 100644 index 0000000000..476c9e0595 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_dependencies.js @@ -0,0 +1,140 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + +const ADDONS = [ + { + id: "addon1@experiments.addons.mozilla.org", + dependencies: ["experiments.addon2"], + }, + { + id: "addon2@experiments.addons.mozilla.org", + dependencies: ["experiments.addon3"], + }, + { + id: "addon3@experiments.addons.mozilla.org", + }, + { + id: "addon4@experiments.addons.mozilla.org", + }, + { + id: "addon5@experiments.addons.mozilla.org", + dependencies: ["experiments.addon2"], + }, +]; + +let addonFiles = []; + +let events = []; + +function promiseAddonStartup(id) { + return new Promise(resolve => { + const onBootstrapMethod = (event, { method, params }) => { + if (method == "startup" && params.id == id) { + AddonTestUtils.off("bootstrap-method", onBootstrapMethod); + resolve(); + } + }; + + AddonTestUtils.on("bootstrap-method", onBootstrapMethod); + }); +} + +add_task(async function setup() { + await promiseStartupManager(); + + const onBootstrapMethod = (event, { method, params }) => { + if (method == "startup" || method == "shutdown") { + events.push([method, params.id]); + } + }; + + AddonTestUtils.on("bootstrap-method", onBootstrapMethod); + registerCleanupFunction(() => { + AddonTestUtils.off("bootstrap-method", onBootstrapMethod); + }); + + for (let addon of ADDONS) { + let manifest = { + browser_specific_settings: { gecko: { id: addon.id } }, + permissions: addon.dependencies, + }; + + addonFiles.push(await createTempWebExtensionFile({ manifest })); + } +}); + +add_task(async function () { + deepEqual(events, [], "Should have no events"); + + await promiseInstallFile(addonFiles[3]); + + deepEqual(events, [["startup", ADDONS[3].id]]); + + events.length = 0; + + await promiseInstallFile(addonFiles[0]); + deepEqual(events, [], "Should have no events"); + + await promiseInstallFile(addonFiles[1]); + deepEqual(events, [], "Should have no events"); + + await Promise.all([ + promiseInstallFile(addonFiles[2]), + promiseAddonStartup(ADDONS[0].id), + ]); + + deepEqual(events, [ + ["startup", ADDONS[2].id], + ["startup", ADDONS[1].id], + ["startup", ADDONS[0].id], + ]); + + events.length = 0; + + await Promise.all([ + promiseInstallFile(addonFiles[2]), + promiseAddonStartup(ADDONS[0].id), + ]); + + deepEqual(events, [ + ["shutdown", ADDONS[0].id], + ["shutdown", ADDONS[1].id], + ["shutdown", ADDONS[2].id], + + ["startup", ADDONS[2].id], + ["startup", ADDONS[1].id], + ["startup", ADDONS[0].id], + ]); + + events.length = 0; + + await promiseInstallFile(addonFiles[4]); + + deepEqual(events, [["startup", ADDONS[4].id]]); + + events.length = 0; + + await promiseRestartManager(); + + deepEqual(events, [ + ["shutdown", ADDONS[4].id], + ["shutdown", ADDONS[3].id], + ["shutdown", ADDONS[0].id], + ["shutdown", ADDONS[1].id], + ["shutdown", ADDONS[2].id], + + ["startup", ADDONS[2].id], + ["startup", ADDONS[1].id], + ["startup", ADDONS[0].id], + ["startup", ADDONS[3].id], + ["startup", ADDONS[4].id], + ]); + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_dictionary_webextension.js b/toolkit/mozapps/extensions/test/xpcshell/test_dictionary_webextension.js new file mode 100644 index 0000000000..1f52c8a8bc --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_dictionary_webextension.js @@ -0,0 +1,263 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +XPCOMUtils.defineLazyServiceGetter( + this, + "spellCheck", + "@mozilla.org/spellchecker/engine;1", + "mozISpellCheckingEngine" +); + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "61", "61"); + + // Initialize the URLPreloader so that we can load the built-in + // add-ons list, which contains the list of built-in dictionaries. + AddonTestUtils.initializeURLPreloader(); + + await promiseStartupManager(); + + // Starts collecting the Addon Manager Telemetry events. + AddonTestUtils.hookAMTelemetryEvents(); + + do_get_profile(); + Services.fog.initializeFOG(); +}); + +add_task( + { + // We need to enable this pref because some assertions verify that + // `installOrigins` is collected in some Telemetry events. + pref_set: [["extensions.install_origins.enabled", true]], + }, + async function test_validation() { + await Assert.rejects( + promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { + gecko: { id: "en-US-no-dic@dictionaries.mozilla.org" }, + }, + dictionaries: { + "en-US": "en-US.dic", + }, + }, + }), + /Expected file to be downloaded for install/ + ); + + await Assert.rejects( + promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { + gecko: { id: "en-US-no-aff@dictionaries.mozilla.org" }, + }, + dictionaries: { + "en-US": "en-US.dic", + }, + }, + + files: { + "en-US.dic": "", + }, + }), + /Expected file to be downloaded for install/ + ); + + let addon = await promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { + gecko: { id: "en-US-1@dictionaries.mozilla.org" }, + }, + dictionaries: { + "en-US": "en-US.dic", + }, + }, + + files: { + "en-US.dic": "", + "en-US.aff": "", + }, + }); + + let addon2 = await promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { + gecko: { id: "en-US-2@dictionaries.mozilla.org" }, + }, + dictionaries: { + "en-US": "dictionaries/en-US.dic", + }, + }, + + files: { + "dictionaries/en-US.dic": "", + "dictionaries/en-US.aff": "", + }, + }); + + await addon.uninstall(); + await addon2.uninstall(); + + let amEvents = AddonTestUtils.getAMTelemetryEvents(); + + let amInstallEvents = amEvents + .filter(evt => evt.method === "install") + .map(evt => { + const { object, extra } = evt; + return { object, extra }; + }); + + const errorExtra = { + step: "started", + error: "ERROR_CORRUPT_FILE", + install_origins: "0", + }; + + Assert.deepEqual( + amInstallEvents.filter(evt => evt.object === "unknown"), + [ + { + object: "unknown", + extra: errorExtra, + }, + { + object: "unknown", + extra: errorExtra, + }, + ], + "Got the expected install telemetry events for the corrupted dictionaries" + ); + + Assert.deepEqual( + AddonTestUtils.getAMGleanEvents("install", { addon_type: "unknown" }), + [ + { addon_type: "unknown", ...errorExtra }, + { addon_type: "unknown", ...errorExtra }, + ], + "Got the expected install Glean events for the corrupted dictionaries" + ); + + const extra1 = { addon_id: addon.id, install_origins: "0" }; + Assert.deepEqual( + amInstallEvents.filter(evt => evt.extra.addon_id === addon.id), + [ + { + object: "dictionary", + extra: { step: "started", ...extra1 }, + }, + { + object: "dictionary", + extra: { step: "completed", ...extra1 }, + }, + ], + "Got the expected install telemetry events for the first installed dictionary" + ); + + Assert.deepEqual( + AddonTestUtils.getAMGleanEvents("install", { addon_id: addon.id }), + [ + { addon_type: "dictionary", step: "started", ...extra1 }, + { addon_type: "dictionary", step: "completed", ...extra1 }, + ], + "Got expected install Glean events for the first installed dictionary." + ); + + const extra2 = { addon_id: addon2.id, install_origins: "0" }; + Assert.deepEqual( + amInstallEvents.filter(evt => evt.extra.addon_id === addon2.id), + [ + { + object: "dictionary", + extra: { step: "started", ...extra2 }, + }, + { + object: "dictionary", + extra: { step: "completed", ...extra2 }, + }, + ], + "Got the expected install telemetry events for the second installed dictionary" + ); + + Assert.deepEqual( + AddonTestUtils.getAMGleanEvents("install", { addon_id: addon2.id }), + [ + { addon_type: "dictionary", step: "started", ...extra2 }, + { addon_type: "dictionary", step: "completed", ...extra2 }, + ] + ); + + let amUninstallEvents = amEvents + .filter(evt => evt.method === "uninstall") + .map(evt => { + const { object, value } = evt; + return { object, value }; + }); + + Assert.deepEqual( + amUninstallEvents, + [ + { object: "dictionary", value: addon.id }, + { object: "dictionary", value: addon2.id }, + ], + "Got the expected uninstall telemetry events" + ); + + Assert.deepEqual( + AddonTestUtils.getAMGleanEvents("manage", { method: "uninstall" }), + [ + { addon_type: "dictionary", addon_id: addon.id, method: "uninstall" }, + { addon_type: "dictionary", addon_id: addon2.id, method: "uninstall" }, + ], + "Got the expected uninstall Glean events." + ); + } +); + +const WORD = "Flehgragh"; + +add_task(async function test_registration() { + spellCheck.dictionaries = ["en-US"]; + + ok(!spellCheck.check(WORD), "Word should not pass check before add-on loads"); + + let addon = await promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { + gecko: { id: "en-US@dictionaries.mozilla.org" }, + }, + dictionaries: { + "en-US": "en-US.dic", + }, + }, + + files: { + "en-US.dic": `2\n${WORD}\nnativ/A\n`, + "en-US.aff": ` +SFX A Y 1 +SFX A 0 en [^elr] + `, + }, + }); + + ok( + spellCheck.check(WORD), + "Word should pass check while add-on load is loaded" + ); + ok(spellCheck.check("nativen"), "Words should have correct affixes"); + + await addon.uninstall(); + + await new Promise(executeSoon); + + ok( + !spellCheck.check(WORD), + "Word should not pass check after add-on unloads" + ); +}); + +add_task(function teardown_telemetry_events() { + // Ignore any additional telemetry events collected in this file. + AddonTestUtils.getAMTelemetryEvents(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_distribution.js b/toolkit/mozapps/extensions/test/xpcshell/test_distribution.js new file mode 100644 index 0000000000..61231160d8 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_distribution.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies that add-ons distributed with the application get installed +// correctly + +// Allow distributed add-ons to install +Services.prefs.setBoolPref("extensions.installDistroAddons", true); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); +const distroDir = gProfD.clone(); +distroDir.append("distribution"); +distroDir.append("extensions"); +registerDirectory("XREAppDist", distroDir.parent); + +async function setOldModificationTime() { + // Make sure the installed extension has an old modification time so any + // changes will be detected + await promiseShutdownManager(); + let extension = gProfD.clone(); + extension.append("extensions"); + extension.append(`${ID}.xpi`); + setExtensionModifiedTime(extension, Date.now() - MAKE_FILE_OLD_DIFFERENCE); + await promiseStartupManager(); +} + +const ID = "addon@tests.mozilla.org"; + +async function writeDistroAddon(version) { + let xpi = await createTempWebExtensionFile({ + manifest: { + version, + browser_specific_settings: { gecko: { id: ID } }, + }, + }); + xpi.copyTo(distroDir, `${ID}.xpi`); +} + +// Tests that on the first startup the add-on gets installed +add_task(async function run_test_1() { + await writeDistroAddon("1.0"); + await promiseStartupManager(); + + let a1 = await AddonManager.getAddonByID(ID); + Assert.notEqual(a1, null); + Assert.equal(a1.version, "1.0"); + Assert.ok(a1.isActive); + Assert.equal(a1.scope, AddonManager.SCOPE_PROFILE); + Assert.ok(!a1.foreignInstall); +}); + +// Tests that starting with a newer version in the distribution dir doesn't +// install it yet +add_task(async function run_test_2() { + await setOldModificationTime(); + + await writeDistroAddon("2.0"); + await promiseRestartManager(); + + let a1 = await AddonManager.getAddonByID(ID); + Assert.notEqual(a1, null); + Assert.equal(a1.version, "1.0"); + Assert.ok(a1.isActive); + Assert.equal(a1.scope, AddonManager.SCOPE_PROFILE); +}); + +// Test that an app upgrade installs the newer version +add_task(async function run_test_3() { + await promiseRestartManager("2"); + + let a1 = await AddonManager.getAddonByID(ID); + Assert.notEqual(a1, null); + Assert.equal(a1.version, "2.0"); + Assert.ok(a1.isActive); + Assert.equal(a1.scope, AddonManager.SCOPE_PROFILE); + Assert.ok(!a1.foreignInstall); +}); + +// Test that an app upgrade doesn't downgrade the extension +add_task(async function run_test_4() { + await setOldModificationTime(); + + await writeDistroAddon("1.0"); + await promiseRestartManager("3"); + + let a1 = await AddonManager.getAddonByID(ID); + Assert.notEqual(a1, null); + Assert.equal(a1.version, "2.0"); + Assert.ok(a1.isActive); + Assert.equal(a1.scope, AddonManager.SCOPE_PROFILE); +}); + +// Tests that after uninstalling a restart doesn't re-install the extension +add_task(async function run_test_5() { + let a1 = await AddonManager.getAddonByID(ID); + await a1.uninstall(); + + await promiseRestartManager(); + + let a1_2 = await AddonManager.getAddonByID(ID); + Assert.equal(a1_2, null); +}); + +// Tests that upgrading the application still doesn't re-install the uninstalled +// extension +add_task(async function run_test_6() { + await promiseRestartManager("4"); + + let a1 = await AddonManager.getAddonByID(ID); + Assert.equal(a1, null); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_distribution_langpack.js b/toolkit/mozapps/extensions/test/xpcshell/test_distribution_langpack.js new file mode 100644 index 0000000000..0e594d60ec --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_distribution_langpack.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies that add-ons distributed with the application in +// langauge subdirectories correctly get installed + +// Allow distributed add-ons to install +Services.prefs.setBoolPref("extensions.installDistroAddons", true); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); +const distroDir = gProfD.clone(); +distroDir.append("distribution"); +distroDir.append("extensions"); +registerDirectory("XREAppDist", distroDir.parent); +const enUSDistroDir = distroDir.clone(); +enUSDistroDir.append("locale-en-US"); +const deDEDistroDir = distroDir.clone(); +deDEDistroDir.append("locale-de-DE"); +const esESDistroDir = distroDir.clone(); +esESDistroDir.append("locale-es-ES"); + +const enUSID = "addon-en-US@tests.mozilla.org"; +const deDEID = "addon-de-DE@tests.mozilla.org"; +const esESID = "addon-es-ES@tests.mozilla.org"; + +async function writeDistroAddons(version) { + let xpi = await createTempWebExtensionFile({ + manifest: { + version, + browser_specific_settings: { gecko: { id: enUSID } }, + }, + }); + xpi.copyTo(enUSDistroDir, `${enUSID}.xpi`); + + xpi = await createTempWebExtensionFile({ + manifest: { + version, + browser_specific_settings: { gecko: { id: deDEID } }, + }, + }); + xpi.copyTo(deDEDistroDir, `${deDEID}.xpi`); + + xpi = await createTempWebExtensionFile({ + manifest: { + version, + browser_specific_settings: { gecko: { id: esESID } }, + }, + }); + xpi.copyTo(esESDistroDir, `${esESID}.xpi`); +} + +add_task(async function setup() { + await writeDistroAddons("1.0"); +}); + +// Tests that on the first startup the requested locale +// add-on gets installed, and others don't. +add_task(async function run_locale_test() { + Services.locale.availableLocales = ["de-DE", "en-US"]; + Services.locale.requestedLocales = ["de-DE"]; + + Assert.equal(Services.locale.requestedLocale, "de-DE"); + + await promiseStartupManager(); + + let a1 = await AddonManager.getAddonByID(deDEID); + Assert.notEqual(a1, null); + Assert.equal(a1.version, "1.0"); + Assert.ok(a1.isActive); + Assert.equal(a1.scope, AddonManager.SCOPE_PROFILE); + Assert.ok(!a1.foreignInstall); + + let a2 = await AddonManager.getAddonByID(enUSID); + Assert.equal(a2, null); + + let a3 = await AddonManager.getAddonByID(esESID); + Assert.equal(a3, null); + + await a1.uninstall(); + await promiseShutdownManager(); +}); + +// Tests that on the first startup the correct fallback locale +// add-on gets installed, and others don't. +add_task(async function run_fallback_test() { + Services.locale.availableLocales = ["es-ES", "en-US"]; + Services.locale.requestedLocales = ["es-UY"]; + + Assert.equal(Services.locale.requestedLocale, "es-UY"); + + await promiseStartupManager(); + + let a1 = await AddonManager.getAddonByID(esESID); + Assert.notEqual(a1, null); + Assert.equal(a1.version, "1.0"); + Assert.ok(a1.isActive); + Assert.equal(a1.scope, AddonManager.SCOPE_PROFILE); + Assert.ok(!a1.foreignInstall); + + let a2 = await AddonManager.getAddonByID(enUSID); + Assert.equal(a2, null); + + let a3 = await AddonManager.getAddonByID(deDEID); + Assert.equal(a3, null); + + await a1.uninstall(); + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_embedderDisabled.js b/toolkit/mozapps/extensions/test/xpcshell/test_embedderDisabled.js new file mode 100644 index 0000000000..943b3cf0c3 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_embedderDisabled.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const ADDON_ID = "embedder-disabled@tests.mozilla.org"; +const PREF_IS_EMBEDDED = "extensions.isembedded"; + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49"); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref(PREF_IS_EMBEDDED); +}); + +async function installExtension() { + return promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { gecko: { id: ADDON_ID } }, + }, + }); +} + +add_task(async function test_setup() { + await promiseStartupManager(); +}); + +add_task(async function embedder_disabled_while_not_embedding() { + const addon = await installExtension(); + let exceptionThrown = false; + try { + await addon.setEmbedderDisabled(true); + } catch (exception) { + exceptionThrown = true; + } + + equal(exceptionThrown, true); + + // Verify that the addon is not affected + equal(addon.isActive, true); + equal(addon.embedderDisabled, undefined); + + await addon.uninstall(); +}); + +add_task(async function unset_embedder_disabled_while_not_embedding() { + Services.prefs.setBoolPref(PREF_IS_EMBEDDED, true); + + const addon = await installExtension(); + await addon.setEmbedderDisabled(true); + + // Verify the addon is not active anymore + equal(addon.isActive, false); + equal(addon.embedderDisabled, true); + + Services.prefs.setBoolPref(PREF_IS_EMBEDDED, false); + + // Verify that embedder disabled cannot be read if not embedding + equal(addon.embedderDisabled, undefined); + + await addon.disable(); + await addon.enable(); + + // Verify that embedder disabled can be removed + equal(addon.isActive, true); + equal(addon.embedderDisabled, undefined); + + await addon.uninstall(); +}); + +add_task(async function embedder_disabled_while_embedding() { + Services.prefs.setBoolPref(PREF_IS_EMBEDDED, true); + + const addon = await installExtension(); + await addon.setEmbedderDisabled(true); + + // Verify the addon is not active anymore + equal(addon.embedderDisabled, true); + equal(addon.isActive, false); + + await addon.setEmbedderDisabled(false); + + // Verify that the addon is now enabled again + equal(addon.isActive, true); + equal(addon.embedderDisabled, false); + await addon.uninstall(); + + Services.prefs.setBoolPref(PREF_IS_EMBEDDED, false); +}); + +add_task(async function embedder_disabled_while_user_disabled() { + Services.prefs.setBoolPref(PREF_IS_EMBEDDED, true); + + const addon = await installExtension(); + await addon.disable(); + + // Verify that the addon is userDisabled + equal(addon.isActive, false); + equal(addon.userDisabled, true); + equal(addon.embedderDisabled, false); + + await addon.setEmbedderDisabled(true); + + // Verify that the addon can be userDisabled and embedderDisabled + equal(addon.isActive, false); + equal(addon.userDisabled, true); + equal(addon.embedderDisabled, true); + + await addon.setEmbedderDisabled(false); + + // Verify that unsetting embedderDisabled doesn't enable the addon + equal(addon.isActive, false); + equal(addon.userDisabled, true); + equal(addon.embedderDisabled, false); + + await addon.enable(); + + // Verify that the addon can be enabled after unsetting userDisabled + equal(addon.isActive, true); + equal(addon.userDisabled, false); + equal(addon.embedderDisabled, false); + + await addon.uninstall(); + + Services.prefs.setBoolPref(PREF_IS_EMBEDDED, false); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_error.js b/toolkit/mozapps/extensions/test/xpcshell/test_error.js new file mode 100644 index 0000000000..ee972f222e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_error.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that various error conditions are handled correctly + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + + await promiseStartupManager(); +}); + +// Checks that a local file validates ok +add_task(async function run_test_1() { + let xpi = await createTempWebExtensionFile({}); + let install = await AddonManager.getInstallForFile(xpi); + Assert.notEqual(install, null); + Assert.equal(install.state, AddonManager.STATE_DOWNLOADED); + Assert.equal(install.error, 0); + + install.cancel(); +}); + +// Checks that a corrupt file shows an error +add_task(async function run_test_2() { + let xpi = AddonTestUtils.allocTempXPIFile(); + await IOUtils.writeUTF8(xpi.path, "this is not a zip file"); + + let install = await AddonManager.getInstallForFile(xpi); + Assert.notEqual(install, null); + Assert.equal(install.state, AddonManager.STATE_DOWNLOAD_FAILED); + Assert.equal(install.error, AddonManager.ERROR_CORRUPT_FILE); +}); + +// Checks that an empty file shows an error +add_task(async function run_test_3() { + let xpi = await AddonTestUtils.createTempXPIFile({}); + let install = await AddonManager.getInstallForFile(xpi); + Assert.notEqual(install, null); + Assert.equal(install.state, AddonManager.STATE_DOWNLOAD_FAILED); + Assert.equal(install.error, AddonManager.ERROR_CORRUPT_FILE); +}); + +// Checks that a file that doesn't match its hash shows an error +add_task(async function run_test_4() { + let xpi = await createTempWebExtensionFile({}); + let url = Services.io.newFileURI(xpi).spec; + let install = await AddonManager.getInstallForURL(url, { hash: "sha1:foo" }); + Assert.notEqual(install, null); + Assert.equal(install.state, AddonManager.STATE_DOWNLOAD_FAILED); + Assert.equal(install.error, AddonManager.ERROR_INCORRECT_HASH); +}); + +// Checks that a file that doesn't exist shows an error +add_task(async function run_test_5() { + let file = do_get_file("data"); + file.append("missing.xpi"); + let install = await AddonManager.getInstallForFile(file); + Assert.notEqual(install, null); + Assert.equal(install.state, AddonManager.STATE_DOWNLOAD_FAILED); + Assert.equal(install.error, AddonManager.ERROR_NETWORK_FAILURE); +}); + +// Checks that an add-on with an illegal ID shows an error +add_task(async function run_test_6() { + let xpi = await createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { gecko: { id: "invalid" } }, + }, + }); + let install = await AddonManager.getInstallForFile(xpi); + Assert.notEqual(install, null); + Assert.equal(install.state, AddonManager.STATE_DOWNLOAD_FAILED); + Assert.equal(install.error, AddonManager.ERROR_CORRUPT_FILE); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js b/toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js new file mode 100644 index 0000000000..9fbff4efe1 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js @@ -0,0 +1,223 @@ +"use strict"; + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "48", "48"); + await promiseStartupManager(); +}); + +/* eslint-disable no-undef */ +// Shared background function for getSelf tests +function backgroundGetSelf() { + browser.management.getSelf().then( + extInfo => { + let url = browser.runtime.getURL("*"); + extInfo.hostPermissions = extInfo.hostPermissions.filter(i => i != url); + + // Internal permissions are currently part of the permissions included + // in the management.getSelf results, and in non release channels + // any temporary installed extension is recognized as privileged + // and some internal permission would be added automatically. + // + // TODO(Bug 1713344): this may become unnecessary if we filter out + // the internal permissions from the management API results. + extInfo.permissions = extInfo.permissions.filter( + i => !i.startsWith("internal:") + ); + + extInfo.url = browser.runtime.getURL(""); + browser.test.sendMessage("management-getSelf", extInfo); + }, + error => { + browser.test.notifyFail(`getSelf rejected with error: ${error}`); + } + ); +} +/* eslint-enable no-undef */ + +add_task(async function test_management_get_self_complete() { + const id = "get_self_test_complete@tests.mozilla.com"; + const permissions = ["management", "cookies"]; + const hostPermissions = ["*://example.org/*", "https://foo.example.org/*"]; + + let manifest = { + browser_specific_settings: { + gecko: { + id, + update_url: "https://updates.mozilla.com/", + }, + }, + name: "test extension name", + short_name: "test extension short name", + description: "test extension description", + version: "1.0", + homepage_url: "http://www.example.com/", + options_ui: { + page: "get_self_options.html", + }, + icons: { + 16: "icons/icon-16.png", + 48: "icons/icon-48.png", + }, + permissions: [...permissions, ...hostPermissions], + }; + + let extension = ExtensionTestUtils.loadExtension({ + manifest, + background: backgroundGetSelf, + useAddonManager: "temporary", + }); + await extension.startup(); + let extInfo = await extension.awaitMessage("management-getSelf"); + + equal(extInfo.id, id, "getSelf returned the expected id"); + equal( + extInfo.installType, + "development", + "getSelf returned the expected installType" + ); + for (let prop of ["name", "description", "version"]) { + equal( + extInfo[prop], + manifest[prop], + `getSelf returned the expected ${prop}` + ); + } + equal( + extInfo.shortName, + manifest.short_name, + "getSelf returned the expected shortName" + ); + equal( + extInfo.mayDisable, + true, + "getSelf returned the expected value for mayDisable" + ); + equal( + extInfo.enabled, + true, + "getSelf returned the expected value for enabled" + ); + equal( + extInfo.homepageUrl, + manifest.homepage_url, + "getSelf returned the expected homepageUrl" + ); + equal( + extInfo.updateUrl, + manifest.browser_specific_settings.gecko.update_url, + "getSelf returned the expected updateUrl" + ); + ok( + extInfo.optionsUrl.endsWith(manifest.options_ui.page), + "getSelf returned the expected optionsUrl" + ); + for (let [index, size] of Object.keys(manifest.icons).sort().entries()) { + let iconUrl = `${extInfo.url}${manifest.icons[size]}`; + equal( + extInfo.icons[index].size, + +size, + "getSelf returned the expected icon size" + ); + equal( + extInfo.icons[index].url, + iconUrl, + "getSelf returned the expected icon url" + ); + } + deepEqual( + extInfo.permissions.sort(), + permissions.sort(), + "getSelf returned the expected permissions" + ); + deepEqual( + extInfo.hostPermissions.sort(), + hostPermissions.sort(), + "getSelf returned the expected hostPermissions" + ); + equal( + extInfo.installType, + "development", + "getSelf returned the expected installType" + ); + await extension.unload(); +}); + +add_task(async function test_management_get_self_minimal() { + const id = "get_self_test_minimal@tests.mozilla.com"; + + let manifest = { + browser_specific_settings: { + gecko: { + id, + }, + }, + name: "test extension name", + version: "1.0", + }; + + let extension = ExtensionTestUtils.loadExtension({ + manifest, + background: backgroundGetSelf, + useAddonManager: "temporary", + }); + await extension.startup(); + let extInfo = await extension.awaitMessage("management-getSelf"); + + equal(extInfo.id, id, "getSelf returned the expected id"); + equal( + extInfo.installType, + "development", + "getSelf returned the expected installType" + ); + for (let prop of ["name", "version"]) { + equal( + extInfo[prop], + manifest[prop], + `getSelf returned the expected ${prop}` + ); + } + for (let prop of ["shortName", "description", "optionsUrl"]) { + equal(extInfo[prop], "", `getSelf returned the expected ${prop}`); + } + for (let prop of ["homepageUrl", " updateUrl", "icons"]) { + equal( + Reflect.getOwnPropertyDescriptor(extInfo, prop), + undefined, + `getSelf did not return a ${prop} property` + ); + } + for (let prop of ["permissions", "hostPermissions"]) { + deepEqual(extInfo[prop], [], `getSelf returned the expected ${prop}`); + } + await extension.unload(); +}); + +add_task(async function test_management_get_self_permanent() { + const id = "get_self_test_permanent@tests.mozilla.com"; + + let manifest = { + browser_specific_settings: { + gecko: { + id, + }, + }, + name: "test extension name", + version: "1.0", + }; + + let extension = ExtensionTestUtils.loadExtension({ + manifest, + background: backgroundGetSelf, + useAddonManager: "permanent", + }); + await extension.startup(); + let extInfo = await extension.awaitMessage("management-getSelf"); + + equal(extInfo.id, id, "getSelf returned the expected id"); + equal( + extInfo.installType, + "normal", + "getSelf returned the expected installType" + ); + await extension.unload(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_filepointer.js b/toolkit/mozapps/extensions/test/xpcshell/test_filepointer.js new file mode 100644 index 0000000000..c72737d4fe --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_filepointer.js @@ -0,0 +1,327 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that various operations with file pointers work and do not affect the +// source files + +const ID1 = "addon1@tests.mozilla.org"; +const ID2 = "addon2@tests.mozilla.org"; + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); +profileDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + +const sourceDir = gProfD.clone(); +sourceDir.append("source"); + +function promiseWriteWebExtension(path, data) { + let files = ExtensionTestCommon.generateFiles(data); + return AddonTestUtils.promiseWriteFilesToDir(path, files); +} + +function promiseWritePointer(aId, aName) { + let path = PathUtils.join(profileDir.path, aName || aId); + + let target = PathUtils.join(sourceDir.path, do_get_expected_addon_name(aId)); + + return IOUtils.writeUTF8(path, target); +} + +function promiseWriteRelativePointer(aId, aName) { + let path = PathUtils.join(profileDir.path, aName || aId); + + let absTarget = sourceDir.clone(); + absTarget.append(do_get_expected_addon_name(aId)); + + let relTarget = absTarget.getRelativeDescriptor(profileDir); + + return IOUtils.writeUTF8(path, relTarget); +} + +add_task(async function setup() { + ok(TEST_UNPACKED, "Pointer files only work with unpacked directories"); + + // Unpacked extensions are never signed, so this can only run with + // signature checks disabled. + Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, false); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); +}); + +// Tests that installing a new add-on by pointer works +add_task(async function test_new_pointer_install() { + let target = PathUtils.join(sourceDir.path, ID1); + await promiseWriteWebExtension(target, { + manifest: { + version: "1.0", + browser_specific_settings: { gecko: { id: ID1 } }, + }, + }); + await promiseWritePointer(ID1); + await promiseStartupManager(); + + let addon = await AddonManager.getAddonByID(ID1); + notEqual(addon, null); + equal(addon.version, "1.0"); + + let file = getAddonFile(addon); + equal(file.parent.path, sourceDir.path); + + let rootUri = do_get_addon_root_uri(sourceDir, ID1); + let uri = addon.getResourceURI(); + equal(uri.spec, rootUri); + + // Check that upgrade is disabled for addons installed by file-pointers. + equal(addon.permissions & AddonManager.PERM_CAN_UPGRADE, 0); +}); + +// Tests that installing the addon from some other source doesn't clobber +// the original sources +add_task(async function test_addon_over_pointer() { + let xpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version: "2.0", + browser_specific_settings: { gecko: { id: ID1 } }, + }, + }); + + let install = await AddonManager.getInstallForFile( + xpi, + "application/x-xpinstall" + ); + await install.install(); + + let addon = await AddonManager.getAddonByID(ID1); + notEqual(addon, null); + equal(addon.version, "2.0"); + + let url = addon.getResourceURI(); + if (url instanceof Ci.nsIJARURI) { + url = url.JARFile; + } + let { file } = url.QueryInterface(Ci.nsIFileURL); + equal(file.parent.path, profileDir.path); + + let rootUri = do_get_addon_root_uri(profileDir, ID1); + let uri = addon.getResourceURI(); + equal(uri.spec, rootUri); + + let source = sourceDir.clone(); + source.append(ID1); + ok(source.exists()); + + await addon.uninstall(); +}); + +// Tests that uninstalling doesn't clobber the original sources +add_task(async function test_uninstall_pointer() { + await promiseWritePointer(ID1); + await promiseRestartManager(); + + let addon = await AddonManager.getAddonByID(ID1); + notEqual(addon, null); + equal(addon.version, "1.0"); + + await addon.uninstall(); + + let source = sourceDir.clone(); + source.append(ID1); + ok(source.exists()); +}); + +// Tests that misnaming a pointer doesn't clobber the sources +add_task(async function test_bad_pointer() { + await promiseWritePointer(ID2, ID1); + + let [a1, a2] = await AddonManager.getAddonsByIDs([ID1, ID2]); + equal(a1, null); + equal(a2, null); + + let source = sourceDir.clone(); + source.append(ID1); + ok(source.exists()); + + let pointer = profileDir.clone(); + pointer.append(ID2); + ok(!pointer.exists()); +}); + +// Tests that changing the ID of an existing add-on doesn't clobber the sources +add_task(async function test_bad_pointer_id() { + let dir = sourceDir.clone(); + dir.append(ID1); + + // Make sure the modification time changes enough to be detected. + setExtensionModifiedTime(dir, dir.lastModifiedTime - 5000); + await promiseWritePointer(ID1); + await promiseRestartManager(); + + let addon = await AddonManager.getAddonByID(ID1); + notEqual(addon, null); + equal(addon.version, "1.0"); + + await promiseWriteWebExtension(dir.path, { + manifest: { + version: "1.0", + browser_specific_settings: { gecko: { id: ID2 } }, + }, + }); + setExtensionModifiedTime(dir, dir.lastModifiedTime - 5000); + + await promiseRestartManager(); + + let [a1, a2] = await AddonManager.getAddonsByIDs([ID1, ID2]); + equal(a1, null); + equal(a2, null); + + let source = sourceDir.clone(); + source.append(ID1); + ok(source.exists()); + + let pointer = profileDir.clone(); + pointer.append(ID1); + ok(!pointer.exists()); +}); + +// Removing the pointer file should uninstall the add-on +add_task(async function test_remove_pointer() { + let dir = sourceDir.clone(); + dir.append(ID1); + + await promiseWriteWebExtension(dir.path, { + manifest: { + version: "1.0", + browser_specific_settings: { gecko: { id: ID1 } }, + }, + }); + + setExtensionModifiedTime(dir, dir.lastModifiedTime - 5000); + await promiseWritePointer(ID1); + + await promiseRestartManager(); + + let addon = await AddonManager.getAddonByID(ID1); + notEqual(addon, null); + equal(addon.version, "1.0"); + + let pointer = profileDir.clone(); + pointer.append(ID1); + pointer.remove(false); + + await promiseRestartManager(); + + addon = await AddonManager.getAddonByID(ID1); + equal(addon, null); +}); + +// Removing the pointer file and replacing it with a directory should work +add_task(async function test_replace_pointer() { + await promiseWritePointer(ID1); + await promiseRestartManager(); + + let addon = await AddonManager.getAddonByID(ID1); + notEqual(addon, null); + equal(addon.version, "1.0"); + + let pointer = profileDir.clone(); + pointer.append(ID1); + pointer.remove(false); + + await promiseWriteWebExtension(PathUtils.join(profileDir.path, ID1), { + manifest: { + version: "2.0", + browser_specific_settings: { gecko: { id: ID1 } }, + }, + }); + + await promiseRestartManager(); + + addon = await AddonManager.getAddonByID(ID1); + notEqual(addon, null); + equal(addon.version, "2.0"); + + await addon.uninstall(); +}); + +// Changes to the source files should be detected +add_task(async function test_change_pointer_sources() { + await promiseWritePointer(ID1); + await promiseRestartManager(); + + let addon = await AddonManager.getAddonByID(ID1); + notEqual(addon, null); + equal(addon.version, "1.0"); + + let dir = sourceDir.clone(); + dir.append(ID1); + await promiseWriteWebExtension(dir.path, { + manifest: { + version: "2.0", + browser_specific_settings: { gecko: { id: ID1 } }, + }, + }); + setExtensionModifiedTime(dir, dir.lastModifiedTime - 5000); + + await promiseRestartManager(); + + addon = await AddonManager.getAddonByID(ID1); + notEqual(addon, null); + equal(addon.version, "2.0"); + + await addon.uninstall(); +}); + +// Removing the add-on the pointer file points at should uninstall the add-on +add_task(async function test_remove_pointer_target() { + let target = PathUtils.join(sourceDir.path, ID1); + await promiseWriteWebExtension(target, { + manifest: { + version: "1.0", + browser_specific_settings: { gecko: { id: ID1 } }, + }, + }); + await promiseWritePointer(ID1); + await promiseRestartManager(); + + let addon = await AddonManager.getAddonByID(ID1); + notEqual(addon, null); + equal(addon.version, "1.0"); + + await IOUtils.remove(target, { recursive: true }); + + await promiseRestartManager(); + + addon = await AddonManager.getAddonByID(ID1); + equal(addon, null); + + let pointer = profileDir.clone(); + pointer.append(ID1); + ok(!pointer.exists()); +}); + +// Tests that installing a new add-on by pointer with a relative path works +add_task(async function test_new_relative_pointer() { + let target = PathUtils.join(sourceDir.path, ID1); + await promiseWriteWebExtension(target, { + manifest: { + version: "1.0", + browser_specific_settings: { gecko: { id: ID1 } }, + }, + }); + await promiseWriteRelativePointer(ID1); + await promiseRestartManager(); + + let addon = await AddonManager.getAddonByID(ID1); + equal(addon.version, "1.0"); + + let { file } = addon.getResourceURI().QueryInterface(Ci.nsIFileURL); + equal(file.parent.path, sourceDir.path); + + let rootUri = do_get_addon_root_uri(sourceDir, ID1); + let uri = addon.getResourceURI(); + equal(uri.spec, rootUri); + + // Check that upgrade is disabled for addons installed by file-pointers. + equal(addon.permissions & AddonManager.PERM_CAN_UPGRADE, 0); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_general.js b/toolkit/mozapps/extensions/test/xpcshell/test_general.js new file mode 100644 index 0000000000..896e69d6f8 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_general.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This just verifies that the EM can actually startup and shutdown a few times +// without any errors + +// We have to look up how many add-ons are present since there will be plugins +// etc. detected +var gCount; + +async function run_test() { + do_test_pending(); + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + + await promiseStartupManager(); + let list = await AddonManager.getAddonsByTypes(null); + gCount = list.length; + + executeSoon(run_test_1); +} + +async function run_test_1() { + await promiseRestartManager(); + + let addons = await AddonManager.getAddonsByTypes(null); + Assert.equal(gCount, addons.length); + + executeSoon(run_test_2); +} + +async function run_test_2() { + await promiseShutdownManager(); + + await promiseStartupManager(); + + let addons = await AddonManager.getAddonsByTypes(null); + Assert.equal(gCount, addons.length); + + executeSoon(run_test_3); +} + +async function run_test_3() { + await promiseRestartManager(); + + let addons = await AddonManager.getAddonsByTypes(null); + Assert.equal(gCount, addons.length); + do_test_finished(); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_getInstallSourceFromHost.js b/toolkit/mozapps/extensions/test/xpcshell/test_getInstallSourceFromHost.js new file mode 100644 index 0000000000..1f9b65ca85 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_getInstallSourceFromHost.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(function test_getInstallSourceFromHost_helpers() { + const test_hostname = + AppConstants.MOZ_APP_NAME !== "thunderbird" + ? "addons.allizom.org" + : "addons-stage.thunderbird.net"; + + const sourceHostTestCases = [ + { + host: test_hostname, + installSourceFromHost: "test-host", + }, + { + host: "addons.mozilla.org", + installSourceFromHost: "amo", + }, + { + host: "discovery.addons.mozilla.org", + installSourceFromHost: "disco", + }, + { + host: "about:blank", + installSourceFromHost: "unknown", + }, + { + host: "fake-extension-uuid", + installSourceFromHost: "unknown", + }, + { + host: null, + installSourceFromHost: "unknown", + }, + ]; + + for (let testCase of sourceHostTestCases) { + let { host, installSourceFromHost } = testCase; + + equal( + AddonManager.getInstallSourceFromHost(host), + installSourceFromHost, + `Got the expected result from getInstallFromHost for host ${host}` + ); + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gmpProvider.js b/toolkit/mozapps/extensions/test/xpcshell/test_gmpProvider.js new file mode 100644 index 0000000000..8f2978c116 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_gmpProvider.js @@ -0,0 +1,477 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { GMPTestUtils } = ChromeUtils.importESModule( + "resource://gre/modules/addons/GMPProvider.sys.mjs" +); +const { GMPInstallManager } = ChromeUtils.importESModule( + "resource://gre/modules/GMPInstallManager.sys.mjs" +); +const { + GMPPrefs, + GMP_PLUGIN_IDS, + OPEN_H264_ID, + WIDEVINE_L1_ID, + WIDEVINE_L3_ID, +} = ChromeUtils.importESModule("resource://gre/modules/GMPUtils.sys.mjs"); +const { UpdateUtils } = ChromeUtils.importESModule( + "resource://gre/modules/UpdateUtils.sys.mjs" +); + +ChromeUtils.defineLazyGetter( + this, + "addonsBundle", + () => new Localization(["toolkit/about/aboutAddons.ftl"]) +); + +var gMockAddons = new Map(); +var gMockEmeAddons = new Map(); + +const mockH264Addon = Object.freeze({ + id: OPEN_H264_ID, + isValid: true, + isInstalled: false, + nameId: "plugins-openh264-name", + descriptionId: "plugins-openh264-description", + libName: "gmpopenh264", + usedFallback: true, +}); +gMockAddons.set(mockH264Addon.id, mockH264Addon); + +const mockWidevineL1Addon = Object.freeze({ + id: WIDEVINE_L1_ID, + isValid: true, + isInstalled: false, + nameId: "plugins-widevine-name", + descriptionId: "plugins-widevine-description", + libName: "Google.Widevine.CDM", + usedFallback: true, +}); +gMockAddons.set(mockWidevineL1Addon.id, mockWidevineL1Addon); +gMockEmeAddons.set(mockWidevineL1Addon.id, mockWidevineL1Addon); + +const mockWidevineAddon = Object.freeze({ + id: WIDEVINE_L3_ID, + isValid: true, + isInstalled: false, + nameId: "plugins-widevine-name", + descriptionId: "plugins-widevine-description", + libName: "widevinecdm", + usedFallback: true, +}); +gMockAddons.set(mockWidevineAddon.id, mockWidevineAddon); +gMockEmeAddons.set(mockWidevineAddon.id, mockWidevineAddon); + +var gInstalledAddonId = ""; +var gPrefs = Services.prefs; +var gGetKey = GMPPrefs.getPrefKey; + +const MockGMPInstallManagerPrototype = { + checkForAddons: () => + Promise.resolve({ + addons: [...gMockAddons.values()], + }), + + installAddon: addon => { + gInstalledAddonId = addon.id; + return Promise.resolve(); + }, +}; + +add_setup(async () => { + Assert.deepEqual( + GMP_PLUGIN_IDS, + Array.from(gMockAddons.keys()), + "set of mock addons matches the actual set of plugins" + ); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + + // The GMPProvider does not register until the first content process + // is launched, so we simulate that by firing this notification. + Services.obs.notifyObservers(null, "ipc:first-content-process-created"); + + await promiseStartupManager(); + + gPrefs.setBoolPref(GMPPrefs.KEY_LOGGING_DUMP, true); + gPrefs.setIntPref(GMPPrefs.KEY_LOGGING_LEVEL, 0); + gPrefs.setBoolPref(GMPPrefs.KEY_EME_ENABLED, true); + for (let addon of gMockAddons.values()) { + gPrefs.setBoolPref(gGetKey(GMPPrefs.KEY_PLUGIN_VISIBLE, addon.id), true); + gPrefs.setBoolPref( + gGetKey(GMPPrefs.KEY_PLUGIN_FORCE_SUPPORTED, addon.id), + true + ); + } +}); + +add_task(async function test_notInstalled() { + for (let addon of gMockAddons.values()) { + gPrefs.setCharPref(gGetKey(GMPPrefs.KEY_PLUGIN_VERSION, addon.id), ""); + gPrefs.setBoolPref(gGetKey(GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), false); + } + + let addons = await promiseAddonsByIDs([...gMockAddons.keys()]); + Assert.equal(addons.length, gMockAddons.size); + + for (let addon of addons) { + Assert.ok(!addon.isInstalled); + Assert.equal(addon.type, "plugin"); + Assert.equal(addon.version, ""); + + let mockAddon = gMockAddons.get(addon.id); + + Assert.notEqual(mockAddon, null); + let name = await addonsBundle.formatValue(mockAddon.nameId); + Assert.equal(addon.name, name); + let description = await addonsBundle.formatValue(mockAddon.descriptionId); + Assert.equal(addon.description, description); + + Assert.ok(!addon.isActive); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.userDisabled); + + Assert.equal( + addon.blocklistState, + Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ); + Assert.equal(addon.scope, AddonManager.SCOPE_APPLICATION); + Assert.equal(addon.pendingOperations, AddonManager.PENDING_NONE); + Assert.equal(addon.operationsRequiringRestart, AddonManager.PENDING_NONE); + + Assert.equal( + addon.permissions, + AddonManager.PERM_CAN_UPGRADE | AddonManager.PERM_CAN_ENABLE + ); + + Assert.equal(addon.updateDate, null); + + Assert.ok(addon.isCompatible); + Assert.ok(addon.isPlatformCompatible); + Assert.ok(addon.providesUpdatesSecurely); + Assert.ok(!addon.foreignInstall); + + let libraries = addon.pluginLibraries; + Assert.ok(libraries); + Assert.equal(libraries.length, 0); + Assert.equal(addon.pluginFullpath, ""); + } +}); + +add_task(async function test_installed() { + const TEST_DATE = new Date(2013, 0, 1, 12); + const TEST_VERSION = "1.2.3.4"; + const TEST_TIME_SEC = Math.round(TEST_DATE.getTime() / 1000); + + let addons = await promiseAddonsByIDs([...gMockAddons.keys()]); + Assert.equal(addons.length, gMockAddons.size); + + for (let addon of addons) { + let mockAddon = gMockAddons.get(addon.id); + Assert.notEqual(mockAddon, null); + + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append(addon.id); + file.append(TEST_VERSION); + gPrefs.setBoolPref( + gGetKey(GMPPrefs.KEY_PLUGIN_ENABLED, mockAddon.id), + false + ); + gPrefs.setIntPref( + gGetKey(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, mockAddon.id), + TEST_TIME_SEC + ); + gPrefs.setCharPref( + gGetKey(GMPPrefs.KEY_PLUGIN_VERSION, mockAddon.id), + TEST_VERSION + ); + + Assert.ok(addon.isInstalled); + Assert.equal(addon.type, "plugin"); + Assert.ok(!addon.isActive); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.userDisabled); + + let name = await addonsBundle.formatValue(mockAddon.nameId); + Assert.equal(addon.name, name); + Assert.equal(addon.version, TEST_VERSION); + + Assert.equal( + addon.permissions, + AddonManager.PERM_CAN_UPGRADE | AddonManager.PERM_CAN_ENABLE + ); + + Assert.equal(addon.updateDate.getTime(), TEST_TIME_SEC * 1000); + + let libraries = addon.pluginLibraries; + Assert.ok(libraries); + Assert.equal(libraries.length, 1); + Assert.equal(libraries[0], TEST_VERSION); + let fullpath = addon.pluginFullpath; + Assert.equal(fullpath.length, 1); + Assert.equal(fullpath[0], file.path); + } +}); + +add_task(async function test_enable() { + let addons = await promiseAddonsByIDs([...gMockAddons.keys()]); + Assert.equal(addons.length, gMockAddons.size); + + for (let addon of addons) { + gPrefs.setBoolPref(gGetKey(GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), true); + + Assert.ok(addon.isActive); + Assert.ok(!addon.appDisabled); + Assert.ok(!addon.userDisabled); + + Assert.equal( + addon.permissions, + AddonManager.PERM_CAN_UPGRADE | AddonManager.PERM_CAN_DISABLE + ); + } +}); + +add_task(async function test_globalEmeDisabled() { + let addons = await promiseAddonsByIDs([...gMockEmeAddons.keys()]); + Assert.equal(addons.length, gMockEmeAddons.size); + + gPrefs.setBoolPref(GMPPrefs.KEY_EME_ENABLED, false); + for (let addon of addons) { + Assert.ok(!addon.isActive); + Assert.ok(addon.appDisabled); + Assert.ok(!addon.userDisabled); + + Assert.equal(addon.permissions, 0); + } + gPrefs.setBoolPref(GMPPrefs.KEY_EME_ENABLED, true); +}); + +add_task(async function test_autoUpdatePrefPersistance() { + let addons = await promiseAddonsByIDs([...gMockAddons.keys()]); + Assert.equal(addons.length, gMockAddons.size); + + for (let addon of addons) { + let autoupdateKey = gGetKey(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id); + gPrefs.clearUserPref(autoupdateKey); + + addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE; + Assert.ok(!gPrefs.getBoolPref(autoupdateKey)); + + addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE; + Assert.equal(addon.applyBackgroundUpdates, AddonManager.AUTOUPDATE_ENABLE); + Assert.ok(gPrefs.getBoolPref(autoupdateKey)); + + addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT; + Assert.ok(!gPrefs.prefHasUserValue(autoupdateKey)); + } +}); + +function createMockPluginFilesIfNeeded(aFile, aPlugin) { + function createFile(aFileName) { + let f = aFile.clone(); + f.append(aFileName); + if (!f.exists()) { + f.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); + } + } + + let libName = + AppConstants.DLL_PREFIX + aPlugin.libName + AppConstants.DLL_SUFFIX; + + createFile(libName); + if (aPlugin.id == WIDEVINE_L1_ID || aPlugin.id == WIDEVINE_L3_ID) { + createFile("manifest.json"); + } else { + createFile(aPlugin.id.substring(4) + ".info"); + } +} + +add_task(async function test_pluginRegistration() { + const TEST_VERSION = "1.2.3.4"; + + let addedPaths = []; + let removedPaths = []; + let clearPaths = () => { + addedPaths = []; + removedPaths = []; + }; + + const MockGMPService = { + addPluginDirectory: path => { + if (!addedPaths.includes(path)) { + addedPaths.push(path); + } + }, + removePluginDirectory: path => { + if (!removedPaths.includes(path)) { + removedPaths.push(path); + } + }, + removeAndDeletePluginDirectory: path => { + if (!removedPaths.includes(path)) { + removedPaths.push(path); + } + }, + }; + + let profD = do_get_profile(); + for (let addon of gMockAddons.values()) { + await GMPTestUtils.overrideGmpService(MockGMPService, () => + testAddon(addon) + ); + } + + async function testAddon(addon) { + let file = profD.clone(); + file.append(addon.id); + file.append(TEST_VERSION); + + gPrefs.setBoolPref(gGetKey(GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), true); + + // Test that plugin registration fails if the plugin dynamic library and + // info files are not present. + gPrefs.setCharPref( + gGetKey(GMPPrefs.KEY_PLUGIN_VERSION, addon.id), + TEST_VERSION + ); + clearPaths(); + await promiseRestartManager(); + Assert.equal(addedPaths.indexOf(file.path), -1); + Assert.deepEqual(removedPaths, [file.path]); + + // Create dummy GMP library/info files, and test that plugin registration + // succeeds during startup, now that we've added GMP info/lib files. + createMockPluginFilesIfNeeded(file, addon); + + gPrefs.setCharPref( + gGetKey(GMPPrefs.KEY_PLUGIN_VERSION, addon.id), + TEST_VERSION + ); + clearPaths(); + await promiseRestartManager(); + Assert.notEqual(addedPaths.indexOf(file.path), -1); + Assert.deepEqual(removedPaths, []); + + // Setting the ABI to something invalid should cause plugin to be removed at startup. + clearPaths(); + gPrefs.setCharPref( + gGetKey(GMPPrefs.KEY_PLUGIN_ABI, addon.id), + "invalid-ABI" + ); + await promiseRestartManager(); + Assert.equal(addedPaths.indexOf(file.path), -1); + Assert.deepEqual(removedPaths, [file.path]); + + // Setting the ABI to expected ABI should cause registration at startup. + clearPaths(); + gPrefs.setCharPref( + gGetKey(GMPPrefs.KEY_PLUGIN_VERSION, addon.id), + TEST_VERSION + ); + gPrefs.setCharPref( + gGetKey(GMPPrefs.KEY_PLUGIN_ABI, addon.id), + UpdateUtils.ABI + ); + await promiseRestartManager(); + Assert.notEqual(addedPaths.indexOf(file.path), -1); + Assert.deepEqual(removedPaths, []); + + // Check that clearing the version doesn't trigger registration. + clearPaths(); + gPrefs.clearUserPref(gGetKey(GMPPrefs.KEY_PLUGIN_VERSION, addon.id)); + Assert.deepEqual(addedPaths, []); + Assert.deepEqual(removedPaths, [file.path]); + + // Restarting with no version set should not trigger registration. + clearPaths(); + await promiseRestartManager(); + Assert.equal(addedPaths.indexOf(file.path), -1); + Assert.equal(removedPaths.indexOf(file.path), -1); + + // Changing the pref mid-session should cause unregistration and registration. + gPrefs.setCharPref( + gGetKey(GMPPrefs.KEY_PLUGIN_VERSION, addon.id), + TEST_VERSION + ); + clearPaths(); + const TEST_VERSION_2 = "5.6.7.8"; + let file2 = Services.dirsvc.get("ProfD", Ci.nsIFile); + file2.append(addon.id); + file2.append(TEST_VERSION_2); + gPrefs.setCharPref( + gGetKey(GMPPrefs.KEY_PLUGIN_VERSION, addon.id), + TEST_VERSION_2 + ); + Assert.deepEqual(addedPaths, [file2.path]); + Assert.deepEqual(removedPaths, [file.path]); + + // Disabling the plugin should cause unregistration. + gPrefs.setCharPref( + gGetKey(GMPPrefs.KEY_PLUGIN_VERSION, addon.id), + TEST_VERSION + ); + clearPaths(); + gPrefs.setBoolPref(gGetKey(GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), false); + Assert.deepEqual(addedPaths, []); + Assert.deepEqual(removedPaths, [file.path]); + + // Restarting with the plugin disabled should not cause registration. + clearPaths(); + await promiseRestartManager(); + Assert.equal(addedPaths.indexOf(file.path), -1); + Assert.equal(removedPaths.indexOf(file.path), -1); + + // Re-enabling the plugin should cause registration. + clearPaths(); + gPrefs.setBoolPref(gGetKey(GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), true); + Assert.deepEqual(addedPaths, [file.path]); + Assert.deepEqual(removedPaths, []); + } +}); + +add_task(async function test_periodicUpdate() { + // The GMPInstallManager constructor has an empty body, + // so replacing the prototype is safe. + let originalInstallManager = GMPInstallManager.prototype; + GMPInstallManager.prototype = MockGMPInstallManagerPrototype; + + let addons = await promiseAddonsByIDs([...gMockAddons.keys()]); + Assert.equal(addons.length, gMockAddons.size); + + for (let addon of addons) { + gPrefs.clearUserPref(gGetKey(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id)); + + addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE; + gPrefs.setIntPref(GMPPrefs.KEY_UPDATE_LAST_CHECK, 0); + let result = await addon.findUpdates( + {}, + AddonManager.UPDATE_WHEN_PERIODIC_UPDATE + ); + Assert.strictEqual(result, false); + + addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE; + gPrefs.setIntPref(GMPPrefs.KEY_UPDATE_LAST_CHECK, Date.now() / 1000 - 60); + result = await addon.findUpdates( + {}, + AddonManager.UPDATE_WHEN_PERIODIC_UPDATE + ); + Assert.strictEqual(result, false); + + const SEC_IN_A_DAY = 24 * 60 * 60; + gPrefs.setIntPref( + GMPPrefs.KEY_UPDATE_LAST_CHECK, + Date.now() / 1000 - 2 * SEC_IN_A_DAY + ); + gInstalledAddonId = ""; + result = await addon.findUpdates( + {}, + AddonManager.UPDATE_WHEN_PERIODIC_UPDATE + ); + Assert.strictEqual(result, true); + Assert.equal(gInstalledAddonId, addon.id); + } + + GMPInstallManager.prototype = originalInstallManager; +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_harness.js b/toolkit/mozapps/extensions/test/xpcshell/test_harness.js new file mode 100644 index 0000000000..8be3cdcf22 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_harness.js @@ -0,0 +1,13 @@ +"use strict"; + +// Test that the test harness is sane. + +// Test that the temporary directory is actually overridden in the +// directory service. +add_task(async function test_TmpD_override() { + equal( + FileUtils.getDir("TmpD", []).path, + AddonTestUtils.tempDir.path, + "Should get the correct temporary directory from the directory service" + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_hidden.js b/toolkit/mozapps/extensions/test/xpcshell/test_hidden.js new file mode 100644 index 0000000000..3d1c187b81 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_hidden.js @@ -0,0 +1,251 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +AddonTestUtils.init(this); +AddonTestUtils.usePrivilegedSignatures = id => id.startsWith("privileged"); + +add_task(async function setup() { + await ExtensionTestUtils.startAddonManager(); +}); + +add_task(async function test_hidden() { + let xpi1 = createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { + gecko: { + id: "privileged@tests.mozilla.org", + }, + }, + + name: "Hidden Extension", + hidden: true, + }, + }); + + let xpi2 = createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { + gecko: { + id: "unprivileged@tests.mozilla.org", + }, + }, + + name: "Non-Hidden Extension", + hidden: true, + }, + }); + + await promiseInstallAllFiles([xpi1, xpi2]); + + let [addon1, addon2] = await promiseAddonsByIDs([ + "privileged@tests.mozilla.org", + "unprivileged@tests.mozilla.org", + ]); + + ok(addon1.isPrivileged, "Privileged is privileged"); + ok(addon1.hidden, "Privileged extension should be hidden"); + ok(!addon2.isPrivileged, "Unprivileged extension is not privileged"); + ok(!addon2.hidden, "Unprivileged extension should not be hidden"); + + await promiseRestartManager(); + + [addon1, addon2] = await promiseAddonsByIDs([ + "privileged@tests.mozilla.org", + "unprivileged@tests.mozilla.org", + ]); + + ok(addon1.isPrivileged, "Privileged is privileged"); + ok(addon1.hidden, "Privileged extension should be hidden"); + ok(!addon2.isPrivileged, "Unprivileged extension is not privileged"); + ok(!addon2.hidden, "Unprivileged extension should not be hidden"); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + browser_specific_settings: { gecko: { id: "privileged@but-temporary" } }, + hidden: true, + }, + }); + await extension.startup(); + let tempAddon = extension.addon; + ok(tempAddon.isPrivileged, "Temporary add-on is privileged"); + ok( + !tempAddon.hidden, + "Temporary add-on is not hidden despite being privileged" + ); + await extension.unload(); +}); + +add_task( + { + pref_set: [["extensions.manifestV3.enabled", true]], + }, + async function test_hidden_and_browser_action_props_are_mutually_exclusive() { + const TEST_CASES = [ + { + title: "hidden and browser_action", + manifest: { + hidden: true, + browser_action: {}, + }, + expectError: true, + }, + { + title: "hidden and page_action", + manifest: { + hidden: true, + page_action: {}, + }, + expectError: true, + }, + { + title: "hidden, browser_action and page_action", + manifest: { + hidden: true, + browser_action: {}, + page_action: {}, + }, + expectError: true, + }, + { + title: "hidden and no browser_action or page_action", + manifest: { + hidden: true, + }, + expectError: false, + }, + { + title: "not hidden and browser_action", + manifest: { + hidden: false, + browser_action: {}, + }, + expectError: false, + }, + { + title: "not hidden and page_action", + manifest: { + hidden: false, + page_action: {}, + }, + expectError: false, + }, + { + title: "no hidden prop and browser_action", + manifest: { + browser_action: {}, + }, + expectError: false, + }, + { + title: "hidden and action", + manifest: { + manifest_version: 3, + hidden: true, + action: {}, + }, + expectError: true, + }, + { + title: "hidden, action and page_action", + manifest: { + manifest_version: 3, + hidden: true, + action: {}, + page_action: {}, + }, + expectError: true, + }, + { + title: "no hidden prop and action", + manifest: { + manifest_version: 3, + action: {}, + }, + expectError: false, + }, + { + title: "no hidden prop and page_action", + manifest: { + page_action: {}, + }, + expectError: false, + }, + { + title: "hidden and action but not privileged", + manifest: { + manifest_version: 3, + hidden: true, + action: {}, + }, + expectError: false, + isPrivileged: false, + }, + { + title: "hidden and browser_action but not privileged", + manifest: { + hidden: true, + browser_action: {}, + }, + expectError: false, + isPrivileged: false, + }, + { + title: "hidden and page_action but not privileged", + manifest: { + hidden: true, + page_action: {}, + }, + expectError: false, + isPrivileged: false, + }, + ]; + + let count = 0; + + for (const { + title, + manifest, + expectError, + isPrivileged = true, + } of TEST_CASES) { + info(`== ${title} ==`); + + // Thunderbird doesn't have page actions. + if (manifest.page_action && AppConstants.MOZ_APP_NAME == "thunderbird") { + continue; + } + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { + id: `${isPrivileged ? "" : "not-"}privileged@ext-${count++}`, + }, + }, + permissions: ["mozillaAddons"], + ...manifest, + }, + background() { + /* globals browser */ + browser.test.sendMessage("ok"); + }, + isPrivileged, + }); + + if (expectError) { + await Assert.rejects( + extension.startup(), + /Cannot use browser and\/or page actions in hidden add-ons/, + "expected extension not started" + ); + } else { + await extension.startup(); + await extension.awaitMessage("ok"); + await extension.unload(); + } + } + } +); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_install.js b/toolkit/mozapps/extensions/test/xpcshell/test_install.js new file mode 100644 index 0000000000..a9ae16fff3 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_install.js @@ -0,0 +1,1063 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testserver = createHttpServer({ hosts: ["example.com"] }); +var gInstallDate; + +const ADDONS = { + test_install1: { + manifest: { + name: "Test 1", + version: "1.0", + browser_specific_settings: { gecko: { id: "addon1@tests.mozilla.org" } }, + }, + }, + test_install2_1: { + manifest: { + name: "Test 2", + version: "2.0", + browser_specific_settings: { gecko: { id: "addon2@tests.mozilla.org" } }, + }, + }, + test_install2_2: { + manifest: { + name: "Test 2", + version: "3.0", + browser_specific_settings: { gecko: { id: "addon2@tests.mozilla.org" } }, + }, + }, + test_install3: { + manifest: { + name: "Test 3", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "addon3@tests.mozilla.org", + strict_min_version: "0", + strict_max_version: "0", + update_url: "http://example.com/update.json", + }, + }, + }, + }, +}; + +const XPIS = {}; + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); +Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false); + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +const UPDATE_JSON = { + addons: { + "addon3@tests.mozilla.org": { + updates: [ + { + version: "1.0", + applications: { + gecko: { + strict_min_version: "0", + strict_max_version: "2", + }, + }, + }, + ], + }, + }, +}; + +const GETADDONS_JSON = { + page_size: 25, + page_count: 1, + count: 1, + next: null, + previous: null, + results: [ + { + name: "Test 2", + type: "extension", + guid: "addon2@tests.mozilla.org", + current_version: { + version: "1.0", + files: [ + { + size: 2, + url: "http://example.com/test_install2_1.xpi", + }, + ], + }, + authors: [ + { + name: "Test Creator", + url: "http://example.com/creator.html", + }, + ], + summary: "Repository summary", + description: "Repository description", + url: "https://addons.mozilla.org/en-US/firefox/addon/addon2@tests.mozilla.org/", + }, + ], +}; + +function checkInstall(install, expected) { + for (let [key, value] of Object.entries(expected)) { + if (value instanceof Ci.nsIURI) { + equal( + install[key] && install[key].spec, + value.spec, + `Expected value of install.${key}` + ); + } else { + deepEqual(install[key], value, `Expected value of install.${key}`); + } + } +} + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + + for (let [name, data] of Object.entries(ADDONS)) { + XPIS[name] = AddonTestUtils.createTempWebExtensionFile(data); + testserver.registerFile(`/addons/${name}.xpi`, XPIS[name]); + } + + await promiseStartupManager(); + + // Create and configure the HTTP server. + AddonTestUtils.registerJSON(testserver, "/update.json", UPDATE_JSON); + testserver.registerDirectory("/data/", do_get_file("data")); + testserver.registerPathHandler("/redirect", function (aRequest, aResponse) { + aResponse.setStatusLine(null, 301, "Moved Permanently"); + let url = aRequest.host + ":" + aRequest.port + aRequest.queryString; + aResponse.setHeader("Location", "http://" + url); + }); + gPort = testserver.identity.primaryPort; +}); + +// Checks that an install from a local file proceeds as expected +add_task(async function test_install_file() { + let [, install] = await Promise.all([ + AddonTestUtils.promiseInstallEvent("onNewInstall"), + AddonManager.getInstallForFile(XPIS.test_install1), + ]); + + let uri = Services.io.newFileURI(XPIS.test_install1); + checkInstall(install, { + type: "extension", + version: "1.0", + name: "Test 1", + state: AddonManager.STATE_DOWNLOADED, + sourceURI: uri, + }); + + let { addon } = install; + checkAddon("addon1@tests.mozilla.org", addon, { + install, + sourceURI: uri, + }); + notEqual(addon.syncGUID, null); + equal( + addon.getResourceURI("manifest.json").spec, + `jar:${uri.spec}!/manifest.json` + ); + + let activeInstalls = await AddonManager.getAllInstalls(); + equal(activeInstalls.length, 1); + equal(activeInstalls[0], install); + + let fooInstalls = await AddonManager.getInstallsByTypes(["foo"]); + equal(fooInstalls.length, 0); + + let extensionInstalls = await AddonManager.getInstallsByTypes(["extension"]); + equal(extensionInstalls.length, 1); + equal(extensionInstalls[0], install); + + await expectEvents( + { + addonEvents: { + "addon1@tests.mozilla.org": [ + { event: "onInstalling" }, + { event: "onInstalled" }, + ], + }, + }, + () => install.install() + ); + + addon = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + ok(addon); + + ok(!hasFlag(addon.permissions, AddonManager.PERM_CAN_ENABLE)); + ok(hasFlag(addon.permissions, AddonManager.PERM_CAN_DISABLE)); + + let updateDate = Date.now(); + + await promiseRestartManager(); + + activeInstalls = await AddonManager.getAllInstalls(); + equal(activeInstalls, 0); + + let a1 = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + let uri2 = do_get_addon_root_uri(profileDir, "addon1@tests.mozilla.org"); + + checkAddon("addon1@tests.mozilla.org", a1, { + type: "extension", + version: "1.0", + name: "Test 1", + foreignInstall: false, + sourceURI: Services.io.newFileURI(XPIS.test_install1), + }); + + notEqual(a1.syncGUID, null); + Assert.greaterOrEqual(a1.syncGUID.length, 9); + + ok(isExtensionInBootstrappedList(profileDir, a1.id)); + ok(XPIS.test_install1.exists()); + do_check_in_crash_annotation(a1.id, a1.version); + + let difference = a1.installDate.getTime() - updateDate; + if (Math.abs(difference) > MAX_TIME_DIFFERENCE) { + do_throw("Add-on install time was out by " + difference + "ms"); + } + + difference = a1.updateDate.getTime() - updateDate; + if (Math.abs(difference) > MAX_TIME_DIFFERENCE) { + do_throw("Add-on update time was out by " + difference + "ms"); + } + + equal(a1.getResourceURI("manifest.json").spec, uri2 + "manifest.json"); + + // Ensure that extension bundle (or icon if unpacked) has updated + // lastModifiedDate. + let testFile = getAddonFile(a1); + ok(testFile.exists()); + difference = testFile.lastModifiedTime - Date.now(); + Assert.less(Math.abs(difference), MAX_TIME_DIFFERENCE); + + await a1.uninstall(); + let { id, version } = a1; + await promiseRestartManager(); + do_check_not_in_crash_annotation(id, version); +}); + +// Tests that an install from a url downloads. +add_task(async function test_install_url() { + let url = "http://example.com/addons/test_install2_1.xpi"; + let install = await AddonManager.getInstallForURL(url, { + name: "Test 2", + version: "1.0", + }); + checkInstall(install, { + version: "1.0", + name: "Test 2", + state: AddonManager.STATE_AVAILABLE, + sourceURI: Services.io.newURI(url), + }); + + let activeInstalls = await AddonManager.getAllInstalls(); + equal(activeInstalls.length, 1); + equal(activeInstalls[0], install); + + await expectEvents( + { + installEvents: [ + { event: "onDownloadStarted" }, + { event: "onDownloadEnded", returnValue: false }, + ], + }, + () => { + install.install(); + } + ); + + checkInstall(install, { + version: "2.0", + name: "Test 2", + state: AddonManager.STATE_DOWNLOADED, + }); + equal(install.addon.install, install); + + await expectEvents( + { + addonEvents: { + "addon2@tests.mozilla.org": [ + { event: "onInstalling" }, + { event: "onInstalled" }, + ], + }, + installEvents: [ + { event: "onInstallStarted" }, + { event: "onInstallEnded" }, + ], + }, + () => install.install() + ); + + let updateDate = Date.now(); + + await promiseRestartManager(); + + let installs = await AddonManager.getAllInstalls(); + equal(installs, 0); + + let a2 = await AddonManager.getAddonByID("addon2@tests.mozilla.org"); + checkAddon("addon2@tests.mozilla.org", a2, { + type: "extension", + version: "2.0", + name: "Test 2", + sourceURI: Services.io.newURI(url), + }); + notEqual(a2.syncGUID, null); + + ok(isExtensionInBootstrappedList(profileDir, a2.id)); + ok(XPIS.test_install2_1.exists()); + do_check_in_crash_annotation(a2.id, a2.version); + + let difference = a2.installDate.getTime() - updateDate; + Assert.lessOrEqual( + Math.abs(difference), + MAX_TIME_DIFFERENCE, + "Add-on install time was correct" + ); + + difference = a2.updateDate.getTime() - updateDate; + Assert.lessOrEqual( + Math.abs(difference), + MAX_TIME_DIFFERENCE, + "Add-on update time was correct" + ); + + gInstallDate = a2.installDate; +}); + +// Tests that installing a new version of an existing add-on works +add_task(async function test_install_new_version() { + let url = "http://example.com/addons/test_install2_2.xpi"; + let [, install] = await Promise.all([ + AddonTestUtils.promiseInstallEvent("onNewInstall"), + AddonManager.getInstallForURL(url, { + name: "Test 2", + version: "3.0", + }), + ]); + + checkInstall(install, { + version: "3.0", + name: "Test 2", + state: AddonManager.STATE_AVAILABLE, + existingAddon: null, + }); + + let activeInstalls = await AddonManager.getAllInstalls(); + equal(activeInstalls.length, 1); + equal(activeInstalls[0], install); + + await expectEvents( + { + installEvents: [ + { event: "onDownloadStarted" }, + { event: "onDownloadEnded", returnValue: false }, + ], + }, + () => { + install.install(); + } + ); + + checkInstall(install, { + version: "3.0", + name: "Test 2", + state: AddonManager.STATE_DOWNLOADED, + existingAddon: await AddonManager.getAddonByID("addon2@tests.mozilla.org"), + }); + + equal(install.addon.install, install); + + // Installation will continue when there is nothing returned. + await expectEvents( + { + addonEvents: { + "addon2@tests.mozilla.org": [ + { event: "onInstalling" }, + { event: "onInstalled" }, + ], + }, + installEvents: [ + { event: "onInstallStarted" }, + { event: "onInstallEnded" }, + ], + }, + () => install.install() + ); + + await promiseRestartManager(); + + let installs2 = await AddonManager.getInstallsByTypes(null); + equal(installs2.length, 0); + + let a2 = await AddonManager.getAddonByID("addon2@tests.mozilla.org"); + checkAddon("addon2@tests.mozilla.org", a2, { + type: "extension", + version: "3.0", + name: "Test 2", + isActive: true, + foreignInstall: false, + sourceURI: Services.io.newURI(url), + installDate: gInstallDate, + }); + + ok(isExtensionInBootstrappedList(profileDir, a2.id)); + ok(XPIS.test_install2_2.exists()); + do_check_in_crash_annotation(a2.id, a2.version); + + // Update date should be later (or the same if this test is too fast) + Assert.lessOrEqual(a2.installDate, a2.updateDate); + + await a2.uninstall(); +}); + +// Tests that an install that requires a compatibility update works +add_task(async function test_install_compat_update() { + let url = "http://example.com/addons/test_install3.xpi"; + let [, install] = await Promise.all([ + AddonTestUtils.promiseInstallEvent("onNewInstall"), + await AddonManager.getInstallForURL(url, { + name: "Test 3", + version: "1.0", + }), + ]); + + checkInstall(install, { + version: "1.0", + name: "Test 3", + state: AddonManager.STATE_AVAILABLE, + }); + + let activeInstalls = await AddonManager.getInstallsByTypes(null); + equal(activeInstalls.length, 1); + equal(activeInstalls[0], install); + + await expectEvents( + { + installEvents: [ + { event: "onDownloadStarted" }, + { event: "onDownloadEnded", returnValue: false }, + ], + }, + () => { + install.install(); + } + ); + + checkInstall(install, { + version: "1.0", + name: "Test 3", + state: AddonManager.STATE_DOWNLOADED, + existingAddon: null, + }); + checkAddon("addon3@tests.mozilla.org", install.addon, { + appDisabled: false, + }); + + // Continue the install + await expectEvents( + { + addonEvents: { + "addon3@tests.mozilla.org": [ + { event: "onInstalling" }, + { event: "onInstalled" }, + ], + }, + installEvents: [ + { event: "onInstallStarted" }, + { event: "onInstallEnded" }, + ], + }, + () => install.install() + ); + + await promiseRestartManager(); + + let installs = await AddonManager.getAllInstalls(); + equal(installs, 0); + + let a3 = await AddonManager.getAddonByID("addon3@tests.mozilla.org"); + checkAddon("addon3@tests.mozilla.org", a3, { + type: "extension", + version: "1.0", + name: "Test 3", + isActive: true, + appDisabled: false, + }); + notEqual(a3.syncGUID, null); + + ok(isExtensionInBootstrappedList(profileDir, a3.id)); + + ok(XPIS.test_install3.exists()); + await a3.uninstall(); +}); + +add_task(async function test_compat_update_local() { + let [, install] = await Promise.all([ + AddonTestUtils.promiseInstallEvent("onNewInstall"), + AddonManager.getInstallForFile(XPIS.test_install3), + ]); + ok(install.addon.isCompatible); + + await expectEvents( + { + addonEvents: { + "addon3@tests.mozilla.org": [ + { event: "onInstalling" }, + { event: "onInstalled" }, + ], + }, + installEvents: [ + { event: "onInstallStarted" }, + { event: "onInstallEnded" }, + ], + }, + () => install.install() + ); + + await promiseRestartManager(); + + let a3 = await AddonManager.getAddonByID("addon3@tests.mozilla.org"); + checkAddon("addon3@tests.mozilla.org", a3, { + type: "extension", + version: "1.0", + name: "Test 3", + isActive: true, + appDisabled: false, + }); + notEqual(a3.syncGUID, null); + + ok(isExtensionInBootstrappedList(profileDir, a3.id)); + + ok(XPIS.test_install3.exists()); + await a3.uninstall(); +}); + +// Test that after cancelling a download it is removed from the active installs +add_task(async function test_cancel() { + let url = "http://example.com/addons/test_install3.xpi"; + let [, install] = await Promise.all([ + AddonTestUtils.promiseInstallEvent("onNewInstall"), + AddonManager.getInstallForURL(url, { + name: "Test 3", + version: "1.0", + }), + ]); + + checkInstall(install, { + version: "1.0", + name: "Test 3", + state: AddonManager.STATE_AVAILABLE, + }); + + let activeInstalls = await AddonManager.getInstallsByTypes(null); + equal(activeInstalls.length, 1); + equal(activeInstalls[0], install); + + let promise; + function cancel() { + promise = expectEvents( + { + installEvents: [{ event: "onDownloadCancelled" }], + }, + () => { + install.cancel(); + } + ); + } + + await expectEvents( + { + installEvents: [ + { event: "onDownloadStarted" }, + { event: "onDownloadEnded", callback: cancel }, + ], + }, + () => { + install.install(); + } + ); + + await promise; + + let file = install.file; + + // Allow the file removal to complete + activeInstalls = await AddonManager.getAllInstalls(); + equal(activeInstalls.length, 0); + ok(!file.exists()); +}); + +// Check that cancelling the install from onDownloadStarted actually cancels it +add_task(async function test_cancel_onDownloadStarted() { + let url = "http://example.com/addons/test_install2_1.xpi"; + let [, install] = await Promise.all([ + AddonTestUtils.promiseInstallEvent("onNewInstall"), + AddonManager.getInstallForURL(url), + ]); + + equal(install.file, null); + + install.addListener({ + onDownloadStarted() { + install.removeListener(this); + executeSoon(() => install.cancel()); + }, + }); + + let promise = AddonTestUtils.promiseInstallEvent("onDownloadCancelled"); + install.install(); + await promise; + + // Wait another tick to see if it continues downloading. + // The listener only really tests if we give it time to see progress, the + // file check isn't ideal either + install.addListener({ + onDownloadProgress() { + do_throw("Download should not have continued"); + }, + onDownloadEnded() { + do_throw("Download should not have continued"); + }, + }); + + let file = install.file; + await Promise.resolve(); + ok(!file.exists()); +}); + +// Checks that cancelling the install from onDownloadEnded actually cancels it +add_task(async function test_cancel_onDownloadEnded() { + let url = "http://example.com/addons/test_install2_1.xpi"; + let [, install] = await Promise.all([ + AddonTestUtils.promiseInstallEvent("onNewInstall"), + AddonManager.getInstallForURL(url), + ]); + + equal(install.file, null); + + let promise; + function cancel() { + promise = expectEvents( + { + installEvents: [{ event: "onDownloadCancelled" }], + }, + async () => { + install.cancel(); + } + ); + } + + await expectEvents( + { + installEvents: [ + { event: "onDownloadStarted" }, + { event: "onDownloadEnded", callback: cancel }, + ], + }, + () => { + install.install(); + } + ); + + await promise; + + install.addListener({ + onInstallStarted() { + do_throw("Install should not have continued"); + }, + }); +}); + +// Verify that the userDisabled value carries over to the upgrade by default +add_task(async function test_userDisabled_update() { + let url = "http://example.com/addons/test_install2_1.xpi"; + let [, install] = await Promise.all([ + AddonTestUtils.promiseInstallEvent("onNewInstall"), + AddonManager.getInstallForURL(url), + ]); + + await install.install(); + + ok(!install.addon.userDisabled); + await install.addon.disable(); + + let addon = await AddonManager.getAddonByID("addon2@tests.mozilla.org"); + checkAddon("addon2@tests.mozilla.org", addon, { + userDisabled: true, + isActive: false, + }); + + url = "http://example.com/addons/test_install2_2.xpi"; + install = await AddonManager.getInstallForURL(url); + await install.install(); + + checkAddon("addon2@tests.mozilla.org", install.addon, { + userDisabled: true, + isActive: false, + }); + + await promiseRestartManager(); + + addon = await AddonManager.getAddonByID("addon2@tests.mozilla.org"); + checkAddon("addon2@tests.mozilla.org", addon, { + userDisabled: true, + isActive: false, + }); + + await addon.uninstall(); +}); + +// Verify that changing the userDisabled value before onInstallEnded works +add_task(async function test_userDisabled() { + let url = "http://example.com/addons/test_install2_1.xpi"; + let install = await AddonManager.getInstallForURL(url); + await install.install(); + + ok(!install.addon.userDisabled); + + let addon = await AddonManager.getAddonByID("addon2@tests.mozilla.org"); + checkAddon("addon2@tests.mozilla.org", addon, { + userDisabled: false, + isActive: true, + }); + + url = "http://example.com/addons/test_install2_2.xpi"; + install = await AddonManager.getInstallForURL(url); + + install.addListener({ + onInstallStarted() { + ok(!install.addon.userDisabled); + install.addon.disable(); + }, + }); + + await install.install(); + + addon = await AddonManager.getAddonByID("addon2@tests.mozilla.org"); + checkAddon("addon2@tests.mozilla.org", addon, { + userDisabled: true, + isActive: false, + }); + + await addon.uninstall(); +}); + +// Checks that metadata is not stored if the pref is set to false +add_task(async function test_18_1() { + AddonTestUtils.registerJSON(testserver, "/getaddons.json", GETADDONS_JSON); + Services.prefs.setCharPref( + PREF_GETADDONS_BYIDS, + "http://example.com/getaddons.json" + ); + + Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", true); + Services.prefs.setBoolPref( + "extensions.addon2@tests.mozilla.org.getAddons.cache.enabled", + false + ); + + let url = "http://example.com/addons/test_install2_1.xpi"; + let install = await AddonManager.getInstallForURL(url); + await install.install(); + + notEqual(install.addon.fullDescription, "Repository description"); + + await promiseRestartManager(); + + let addon = await AddonManager.getAddonByID("addon2@tests.mozilla.org"); + notEqual(addon.fullDescription, "Repository description"); + + await addon.uninstall(); +}); + +// Checks that metadata is downloaded for new installs and is visible before and +// after restart +add_task(async function test_metadata() { + Services.prefs.setBoolPref( + "extensions.addon2@tests.mozilla.org.getAddons.cache.enabled", + true + ); + + let url = "http://example.com/addons/test_install2_1.xpi"; + let install = await AddonManager.getInstallForURL(url); + await install.install(); + + equal(install.addon.fullDescription, "Repository description"); + equal( + install.addon.amoListingURL, + "https://addons.mozilla.org/en-US/firefox/addon/addon2@tests.mozilla.org/" + ); + + await promiseRestartManager(); + + let addon = await AddonManager.getAddonByID("addon2@tests.mozilla.org"); + equal(addon.fullDescription, "Repository description"); + equal( + addon.amoListingURL, + "https://addons.mozilla.org/en-US/firefox/addon/addon2@tests.mozilla.org/" + ); + + await addon.uninstall(); +}); + +// Do the same again to make sure it works when the data is already in the cache +add_task(async function test_metadata_again() { + let url = "http://example.com/addons/test_install2_1.xpi"; + let install = await AddonManager.getInstallForURL(url); + await install.install(); + + equal(install.addon.fullDescription, "Repository description"); + + await promiseRestartManager(); + + let addon = await AddonManager.getAddonByID("addon2@tests.mozilla.org"); + equal(addon.fullDescription, "Repository description"); + equal( + addon.amoListingURL, + "https://addons.mozilla.org/en-US/firefox/addon/addon2@tests.mozilla.org/" + ); + + await addon.uninstall(); +}); + +// Tests that an install can be restarted after being cancelled +add_task(async function test_restart() { + let url = "http://example.com/addons/test_install1.xpi"; + let install = await AddonManager.getInstallForURL(url); + equal(install.state, AddonManager.STATE_AVAILABLE); + + install.addListener({ + onDownloadEnded() { + install.removeListener(this); + install.cancel(); + }, + }); + + try { + await install.install(); + ok(false, "Install should not have succeeded"); + } catch (err) {} + + let promise = expectEvents( + { + addonEvents: { + "addon1@tests.mozilla.org": [ + { event: "onInstalling" }, + { event: "onInstalled" }, + ], + }, + installEvents: [ + { event: "onDownloadStarted" }, + { event: "onDownloadEnded" }, + { event: "onInstallStarted" }, + { event: "onInstallEnded" }, + ], + }, + () => { + install.install(); + } + ); + + await Promise.all([ + promise, + promiseWebExtensionStartup("addon1@tests.mozilla.org"), + ]); + + await install.addon.uninstall(); +}); + +// Tests that an install can be restarted after being cancelled when a hash +// was provided +add_task(async function test_restart_hash() { + let url = "http://example.com/addons/test_install1.xpi"; + let install = await AddonManager.getInstallForURL(url, { + hash: do_get_file_hash(XPIS.test_install1), + }); + equal(install.state, AddonManager.STATE_AVAILABLE); + + install.addListener({ + onDownloadEnded() { + install.removeListener(this); + install.cancel(); + }, + }); + + try { + await install.install(); + ok(false, "Install should not have succeeded"); + } catch (err) {} + + let promise = expectEvents( + { + addonEvents: { + "addon1@tests.mozilla.org": [ + { event: "onInstalling" }, + { event: "onInstalled" }, + ], + }, + installEvents: [ + { event: "onDownloadStarted" }, + { event: "onDownloadEnded" }, + { event: "onInstallStarted" }, + { event: "onInstallEnded" }, + ], + }, + () => { + install.install(); + } + ); + + await Promise.all([ + promise, + promiseWebExtensionStartup("addon1@tests.mozilla.org"), + ]); + + await install.addon.uninstall(); +}); + +// Tests that an install with a bad hash can be restarted after it fails, though +// it will only fail again +add_task(async function test_restart_badhash() { + let url = "http://example.com/addons/test_install1.xpi"; + let install = await AddonManager.getInstallForURL(url, { hash: "sha1:foo" }); + equal(install.state, AddonManager.STATE_AVAILABLE); + + install.addListener({ + onDownloadEnded() { + install.removeListener(this); + install.cancel(); + }, + }); + + try { + await install.install(); + ok(false, "Install should not have succeeded"); + } catch (err) {} + + try { + await install.install(); + ok(false, "Install should not have succeeded"); + } catch (err) { + ok(true, "Resumed install should have failed"); + } +}); + +// Tests that installs with a hash for a local file work +add_task(async function test_local_hash() { + let url = Services.io.newFileURI(XPIS.test_install1).spec; + let install = await AddonManager.getInstallForURL(url, { + hash: do_get_file_hash(XPIS.test_install1), + }); + + checkInstall(install, { + state: AddonManager.STATE_DOWNLOADED, + error: 0, + }); + + install.cancel(); +}); + +// Test that an install cannot be canceled after the install is completed. +add_task(async function test_cancel_completed() { + let url = "http://example.com/addons/test_install1.xpi"; + let install = await AddonManager.getInstallForURL(url); + + let cancelPromise = new Promise((resolve, reject) => { + install.addListener({ + onInstallEnded() { + try { + install.cancel(); + reject("Cancel should fail."); + } catch (e) { + resolve(); + } + }, + }); + }); + + install.install(); + await cancelPromise; + + equal(install.state, AddonManager.STATE_INSTALLED); +}); + +// Test that an install may be canceled after a redirect. +add_task(async function test_cancel_redirect() { + let url = "http://example.com/redirect?/addons/test_install1.xpi"; + let install = await AddonManager.getInstallForURL(url); + + install.addListener({ + onDownloadProgress() { + install.cancel(); + }, + }); + + let promise = AddonTestUtils.promiseInstallEvent("onDownloadCancelled"); + + install.install(); + await promise; + + equal(install.state, AddonManager.STATE_CANCELLED); +}); + +// Tests that an install can be restarted during onDownloadCancelled after being +// cancelled in mid-download +add_task(async function test_restart2() { + let url = "http://example.com/addons/test_install1.xpi"; + let install = await AddonManager.getInstallForURL(url); + + equal(install.state, AddonManager.STATE_AVAILABLE); + + install.addListener({ + onDownloadProgress() { + install.removeListener(this); + install.cancel(); + }, + }); + + let promise = AddonTestUtils.promiseInstallEvent("onDownloadCancelled"); + install.install(); + await promise; + + equal(install.state, AddonManager.STATE_CANCELLED); + + promise = expectEvents( + { + addonEvents: { + "addon1@tests.mozilla.org": [ + { event: "onInstalling" }, + { event: "onInstalled" }, + ], + }, + installEvents: [ + { event: "onDownloadStarted" }, + { event: "onDownloadEnded" }, + { event: "onInstallStarted" }, + { event: "onInstallEnded" }, + ], + }, + () => { + let file = install.file; + install.install(); + notEqual(file.path, install.file.path); + ok(!file.exists()); + } + ); + + await Promise.all([ + promise, + promiseWebExtensionStartup("addon1@tests.mozilla.org"), + ]); + + await install.addon.uninstall(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_installOrigins.js b/toolkit/mozapps/extensions/test/xpcshell/test_installOrigins.js new file mode 100644 index 0000000000..7ef584b54d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_installOrigins.js @@ -0,0 +1,549 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.usePrivilegedSignatures = false; +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); +// This pref is not set in Thunderbird, and needs to be true for the test to pass. +Services.prefs.setBoolPref("extensions.postDownloadThirdPartyPrompt", true); + +let server = AddonTestUtils.createHttpServer({ + hosts: ["example.com", "example.org", "amo.example.com", "github.io"], +}); + +server.registerFile( + `/addons/origins.xpi`, + AddonTestUtils.createTempXPIFile({ + "manifest.json": { + manifest_version: 2, + name: "Install Origins test", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "origins@example.com", + }, + }, + install_origins: ["http://example.com"], + }, + }) +); + +server.registerFile( + `/addons/sitepermission.xpi`, + AddonTestUtils.createTempXPIFile({ + "manifest.json": { + manifest_version: 2, + name: "Install Origins sitepermission test", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "sitepermission@example.com", + }, + }, + install_origins: ["http://example.com"], + site_permissions: ["midi"], + }, + }) +); + +server.registerFile( + `/addons/sitepermission-suffix.xpi`, + AddonTestUtils.createTempXPIFile({ + "manifest.json": { + manifest_version: 2, + name: "Install Origins sitepermission public suffix test", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "sitepermission-suffix@github.io", + }, + }, + install_origins: ["http://github.io"], + site_permissions: ["midi"], + }, + }) +); + +server.registerFile( + `/addons/two_origins.xpi`, + AddonTestUtils.createTempXPIFile({ + "manifest.json": { + manifest_version: 2, + name: "Install Origins test", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "two_origins@example.com", + }, + }, + install_origins: ["http://example.com", "http://example.org"], + }, + }) +); + +server.registerFile( + `/addons/no_origins.xpi`, + AddonTestUtils.createTempXPIFile({ + "manifest.json": { + manifest_version: 2, + name: "Install Origins test", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "no_origins@example.com", + }, + }, + }, + }) +); + +server.registerFile( + `/addons/empty_origins.xpi`, + AddonTestUtils.createTempXPIFile({ + "manifest.json": { + manifest_version: 2, + name: "Install Origins test", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "no_origins@example.com", + }, + }, + install_origins: [], + }, + }) +); + +server.registerFile( + `/addons/v3_origins.xpi`, + AddonTestUtils.createTempXPIFile({ + "manifest.json": { + manifest_version: 3, + name: "Install Origins test", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "v3_origins@example.com", + }, + }, + install_origins: ["http://example.com"], + }, + }) +); + +server.registerFile( + `/addons/v3_no_origins.xpi`, + AddonTestUtils.createTempXPIFile({ + "manifest.json": { + manifest_version: 3, + name: "Install Origins test", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "v3_no_origins@example.com", + }, + }, + }, + }) +); + +add_setup(() => { + do_get_profile(); + Services.fog.initializeFOG(); +}); + +function testInstallEvent(expectTelemetry) { + const snapshot = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ); + + ok( + snapshot.parent && !!snapshot.parent.length, + "Got parent telemetry events in the snapshot" + ); + + let events = snapshot.parent + .filter( + ([timestamp, category, method, object, value, extra]) => + category === "addonsManager" && + method == "install" && + extra.step == expectTelemetry.step + ) + .map(event => event[5]); + equal(events.length, 1, "one event for install completion"); + Assert.deepEqual(events[0], expectTelemetry, "telemetry matches"); + + let gleanEvents = AddonTestUtils.getAMGleanEvents("install", { + step: expectTelemetry.step, + }); + Services.fog.testResetFOG(); + + equal(gleanEvents.length, 1, "One glean event for install completion."); + delete gleanEvents[0].addon_type; + Assert.deepEqual(gleanEvents[0], expectTelemetry, "Glean telemetry matches."); +} + +function promiseCompleteWebInstall( + install, + triggeringPrincipal, + expectPrompts = true +) { + let listener; + return new Promise(_resolve => { + let resolve = () => { + install.removeListener(listener); + _resolve(); + }; + + listener = { + onDownloadFailed: resolve, + onDownloadCancelled: resolve, + onInstallFailed: resolve, + onInstallCancelled: resolve, + onInstallEnded: resolve, + onInstallPostponed: resolve, + }; + + install.addListener(listener); + + // Observers to bypass panels and continue install. + if (expectPrompts) { + TestUtils.topicObserved("addon-install-blocked").then(([subject]) => { + let installInfo = subject.wrappedJSObject; + info(`==== test got addon-install-blocked ${subject} ${installInfo}`); + installInfo.install(); + }); + + TestUtils.topicObserved("addon-install-confirmation").then( + (subject, data) => { + info(`==== test got addon-install-confirmation`); + let installInfo = subject.wrappedJSObject; + for (let installer of installInfo.installs) { + installer.install(); + } + } + ); + TestUtils.topicObserved("webextension-permission-prompt").then( + ([subject]) => { + const { info } = subject.wrappedJSObject || {}; + info.resolve(); + } + ); + } + + AddonManager.installAddonFromWebpage( + "application/x-xpinstall", + null /* aBrowser */, + triggeringPrincipal, + install + ); + }); +} + +async function testAddonInstall(test) { + let { name, xpiUrl, installPrincipal, expectState, expectTelemetry } = test; + info(`testAddonInstall: ${name}`); + let expectInstall = expectState == AddonManager.STATE_INSTALLED; + let install = await AddonManager.getInstallForURL(xpiUrl, { + triggeringPrincipal: installPrincipal, + }); + await promiseCompleteWebInstall(install, installPrincipal, expectInstall); + + // Test origins telemetry + testInstallEvent(expectTelemetry); + + if (expectInstall) { + equal( + install.state, + expectState, + `${name} ${install.addon.id} install was completed` + ); + // Wait the extension startup to ensure manifest.json has been read, + // otherwise we get NS_ERROR_FILE_NOT_FOUND log spam. + await WebExtensionPolicy.getByID(install.addon.id)?.readyPromise; + await install.addon.uninstall(); + } else { + equal( + install.state, + expectState, + `${name} ${install.addon?.id} install failed` + ); + } +} + +let ssm = Services.scriptSecurityManager; +const PRINCIPAL_AMO = ssm.createContentPrincipalFromOrigin( + "https://amo.example.com" +); +const PRINCIPAL_COM = + ssm.createContentPrincipalFromOrigin("http://example.com"); +const SUB_PRINCIPAL_COM = ssm.createContentPrincipalFromOrigin( + "http://abc.example.com" +); +const THIRDPARTY_PRINCIPAL_COM = ssm.createContentPrincipalFromOrigin( + "http://fake-example.com" +); +const PRINCIPAL_ORG = + ssm.createContentPrincipalFromOrigin("http://example.org"); +const PRINCIPAL_ETLD = ssm.createContentPrincipalFromOrigin("http://github.io"); + +const TESTS = [ + { + name: "Install MV2 with install_origins", + xpiUrl: "http://example.com/addons/origins.xpi", + installPrincipal: PRINCIPAL_COM, + expectState: AddonManager.STATE_INSTALLED, + expectTelemetry: { + step: "completed", + addon_id: "origins@example.com", + install_origins: "1", + }, + }, + { + name: "Install MV2 without install_origins", + xpiUrl: "http://example.com/addons/no_origins.xpi", + installPrincipal: PRINCIPAL_COM, + expectState: AddonManager.STATE_INSTALLED, + expectTelemetry: { + step: "completed", + addon_id: "no_origins@example.com", + install_origins: "0", + }, + }, + { + name: "Install valid xpi location from invalid website", + xpiUrl: "http://example.com/addons/origins.xpi", + installPrincipal: PRINCIPAL_ORG, + expectState: AddonManager.STATE_INSTALL_FAILED, + expectTelemetry: { + step: "failed", + addon_id: "origins@example.com", + error: "ERROR_INVALID_DOMAIN", + install_origins: "1", + }, + }, + { + name: "Install invalid xpi location from valid website", + xpiUrl: "http://example.org/addons/origins.xpi", + installPrincipal: PRINCIPAL_COM, + expectState: AddonManager.STATE_INSTALL_FAILED, + expectTelemetry: { + step: "failed", + addon_id: "origins@example.com", + error: "ERROR_INVALID_DOMAIN", + install_origins: "1", + }, + }, + { + name: "Install MV3 with install_origins", + xpiUrl: "http://example.com/addons/v3_origins.xpi", + installPrincipal: PRINCIPAL_COM, + expectState: AddonManager.STATE_INSTALLED, + expectTelemetry: { + step: "completed", + addon_id: "v3_origins@example.com", + install_origins: "1", + }, + }, + { + name: "Install MV3 with install_origins from AMO", + xpiUrl: "http://example.com/addons/v3_origins.xpi", + installPrincipal: PRINCIPAL_AMO, + expectState: AddonManager.STATE_INSTALLED, + expectTelemetry: { + step: "completed", + addon_id: "v3_origins@example.com", + install_origins: "1", + }, + }, + { + name: "Install MV3 without install_origins", + xpiUrl: "http://example.com/addons/v3_no_origins.xpi", + installPrincipal: PRINCIPAL_COM, + expectState: AddonManager.STATE_INSTALL_FAILED, + expectTelemetry: { + step: "failed", + addon_id: "v3_no_origins@example.com", + error: "ERROR_INVALID_DOMAIN", + install_origins: "0", + }, + }, + { + // An installing principal with install permission is + // considered "AMO" in code, and will always be allowed. + name: "Install MV3 without install_origins from AMO", + xpiUrl: "http://example.com/addons/v3_no_origins.xpi", + installPrincipal: PRINCIPAL_AMO, + expectState: AddonManager.STATE_INSTALLED, + expectTelemetry: { + step: "completed", + addon_id: "v3_no_origins@example.com", + install_origins: "0", + }, + }, + { + name: "Install MV3 without install_origins from null principal", + xpiUrl: "http://example.com/addons/v3_no_origins.xpi", + installPrincipal: Services.scriptSecurityManager.createNullPrincipal({}), + expectState: AddonManager.STATE_CANCELLED, + expectTelemetry: { step: "site_blocked", install_origins: "0" }, + }, + { + name: "Install addon with two install_origins", + xpiUrl: "http://example.com/addons/two_origins.xpi", + installPrincipal: PRINCIPAL_ORG, + expectState: AddonManager.STATE_INSTALLED, + expectTelemetry: { + step: "completed", + addon_id: "two_origins@example.com", + install_origins: "1", + }, + }, + { + name: "Install addon with two install_origins", + xpiUrl: "http://example.com/addons/two_origins.xpi", + installPrincipal: PRINCIPAL_COM, + expectState: AddonManager.STATE_INSTALLED, + expectTelemetry: { + step: "completed", + addon_id: "two_origins@example.com", + install_origins: "1", + }, + }, + { + name: "Install from site with empty install_origins", + xpiUrl: "http://example.com/addons/empty_origins.xpi", + installPrincipal: PRINCIPAL_COM, + expectState: AddonManager.STATE_INSTALL_FAILED, + expectTelemetry: { + step: "failed", + addon_id: "no_origins@example.com", + error: "ERROR_INVALID_DOMAIN", + install_origins: "1", + }, + }, + { + name: "Install from site with empty install_origins", + xpiUrl: "http://example.com/addons/empty_origins.xpi", + installPrincipal: PRINCIPAL_ORG, + expectState: AddonManager.STATE_INSTALL_FAILED, + expectTelemetry: { + step: "failed", + addon_id: "no_origins@example.com", + error: "ERROR_INVALID_DOMAIN", + install_origins: "1", + }, + }, + { + name: "Install with empty install_origins from AMO", + xpiUrl: "http://amo.example.com/addons/empty_origins.xpi", + installPrincipal: PRINCIPAL_AMO, + expectState: AddonManager.STATE_INSTALLED, + expectTelemetry: { + step: "completed", + addon_id: "no_origins@example.com", + install_origins: "1", + }, + }, + { + name: "Install sitepermission from domain", + xpiUrl: "http://example.com/addons/sitepermission.xpi", + installPrincipal: PRINCIPAL_COM, + expectState: AddonManager.STATE_INSTALLED, + expectTelemetry: { + step: "completed", + addon_id: "sitepermission@example.com", + install_origins: "1", + }, + }, + { + name: "Install sitepermission from subdomain", + xpiUrl: "http://example.com/addons/sitepermission.xpi", + installPrincipal: SUB_PRINCIPAL_COM, + expectState: AddonManager.STATE_INSTALLED, + expectTelemetry: { + step: "completed", + addon_id: "sitepermission@example.com", + install_origins: "1", + }, + }, + { + name: "Install sitepermission from thirdparty domain should fail", + xpiUrl: "http://example.com/addons/sitepermission.xpi", + installPrincipal: THIRDPARTY_PRINCIPAL_COM, + expectState: AddonManager.STATE_INSTALL_FAILED, + expectTelemetry: { + step: "failed", + addon_id: "sitepermission@example.com", + error: "ERROR_INVALID_DOMAIN", + install_origins: "1", + }, + }, + { + name: "Install sitepermission from different domain", + xpiUrl: "http://example.com/addons/sitepermission.xpi", + installPrincipal: PRINCIPAL_ORG, + expectState: AddonManager.STATE_INSTALL_FAILED, + expectTelemetry: { + step: "failed", + addon_id: "sitepermission@example.com", + error: "ERROR_INVALID_DOMAIN", + install_origins: "1", + }, + }, + { + name: "Install sitepermission from public suffix domain", + xpiUrl: "http://github.io/addons/sitepermission-suffix.xpi", + installPrincipal: PRINCIPAL_ETLD, + expectState: AddonManager.STATE_INSTALL_FAILED, + expectTelemetry: { + step: "failed", + addon_id: "sitepermission-suffix@github.io", + error: "ERROR_INVALID_DOMAIN", + install_origins: "1", + }, + }, +]; + +add_task(async function test_install_url() { + Services.prefs.setBoolPref("extensions.install_origins.enabled", true); + PermissionTestUtils.add( + PRINCIPAL_AMO, + "install", + Services.perms.ALLOW_ACTION + ); + await promiseStartupManager(); + + for (let test of TESTS) { + await testAddonInstall(test); + } +}); + +add_task(async function test_install_origins_disabled() { + Services.prefs.setBoolPref("extensions.install_origins.enabled", false); + await testAddonInstall({ + name: "Install MV3 without install_origins, verification disabled", + xpiUrl: "http://example.com/addons/v3_no_origins.xpi", + installPrincipal: PRINCIPAL_COM, + expectState: AddonManager.STATE_INSTALLED, + expectTelemetry: { + step: "completed", + addon_id: "v3_no_origins@example.com", + }, + }); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_install_cancel.js b/toolkit/mozapps/extensions/test/xpcshell/test_install_cancel.js new file mode 100644 index 0000000000..0be6ec0359 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_install_cancel.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +var testserver = createHttpServer({ hosts: ["example.com"] }); + +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "1.9.2" +); + +class TestListener { + constructor(listener) { + this.listener = listener; + } + + onDataAvailable(...args) { + this.origListener.onDataAvailable(...args); + } + + onStartRequest(request) { + this.origListener.onStartRequest(request); + } + + onStopRequest(request, status) { + if (this.listener.onStopRequest) { + this.listener.onStopRequest(request, status); + } + this.origListener.onStopRequest(request, status); + } +} + +function startListener(listener) { + let observer = { + observe(subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + if (channel.URI.spec === "http://example.com/addons/test.xpi") { + let channelListener = new TestListener(listener); + channelListener.origListener = subject + .QueryInterface(Ci.nsITraceableChannel) + .setNewListener(channelListener); + Services.obs.removeObserver(observer, "http-on-modify-request"); + } + }, + }; + Services.obs.addObserver(observer, "http-on-modify-request"); +} + +add_task(async function setup() { + let xpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + name: "Test", + version: "1.0", + browser_specific_settings: { gecko: { id: "cancel@test" } }, + }, + }); + testserver.registerFile(`/addons/test.xpi`, xpi); + await AddonTestUtils.promiseStartupManager(); +}); + +// This test checks that canceling an install after the download is completed fails +// and throws an exception as expected +add_task(async function test_install_cancelled() { + let url = "http://example.com/addons/test.xpi"; + let install = await AddonManager.getInstallForURL(url, { + name: "Test", + version: "1.0", + }); + + let cancelInstall = new Promise(resolve => { + startListener({ + onStopRequest() { + resolve(Promise.resolve().then(() => install.cancel())); + }, + }); + }); + + await install.install().then(() => { + ok(true, "install succeeded"); + }); + + await cancelInstall + .then(() => { + ok(false, "cancel should not succeed"); + }) + .catch(e => { + ok(!!e, "cancel threw an exception"); + }); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_install_file_change.js b/toolkit/mozapps/extensions/test/xpcshell/test_install_file_change.js new file mode 100644 index 0000000000..75cc91038e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_install_file_change.js @@ -0,0 +1,180 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +const server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false); + +/* globals browser */ + +add_task(async function setup() { + await ExtensionTestUtils.startAddonManager(); +}); + +async function createXPIWithID(addonId, version = "1.0") { + let xpiFile = await createTempWebExtensionFile({ + manifest: { + version, + browser_specific_settings: { gecko: { id: addonId } }, + }, + }); + return xpiFile; +} + +const ERROR_PATTERN_INSTALL_FAIL = /Failed to install .+ from .+ to /; +const ERROR_PATTERN_POSTPONE_FAIL = /Failed to postpone install of /; + +async function promiseInstallFail(install, expectedErrorPattern) { + let { messages } = await promiseConsoleOutput(async () => { + await Assert.rejects( + install.install(), + /^Error: Install failed: onInstallFailed$/ + ); + }); + messages = messages.filter(msg => expectedErrorPattern.test(msg.message)); + equal(messages.length, 1, "Expected log messages"); + equal(install.state, AddonManager.STATE_INSTALL_FAILED); + equal(install.error, AddonManager.ERROR_FILE_ACCESS); + equal((await AddonManager.getAllInstalls()).length, 0, "no pending installs"); +} + +add_task(async function test_file_deleted() { + let xpiFile = await createXPIWithID("delete@me"); + let install = await AddonManager.getInstallForFile(xpiFile); + equal(install.state, AddonManager.STATE_DOWNLOADED); + + xpiFile.remove(false); + + await promiseInstallFail(install, ERROR_PATTERN_INSTALL_FAIL); + + equal(await AddonManager.getAddonByID("delete@me"), null); +}); + +add_task(async function test_file_emptied() { + let xpiFile = await createXPIWithID("empty@me"); + let install = await AddonManager.getInstallForFile(xpiFile); + equal(install.state, AddonManager.STATE_DOWNLOADED); + + await IOUtils.write(xpiFile.path, new Uint8Array()); + + await promiseInstallFail(install, ERROR_PATTERN_INSTALL_FAIL); + + equal(await AddonManager.getAddonByID("empty@me"), null); +}); + +add_task(async function test_file_replaced() { + let xpiFile = await createXPIWithID("replace@me"); + let install = await AddonManager.getInstallForFile(xpiFile); + equal(install.state, AddonManager.STATE_DOWNLOADED); + + await IOUtils.copy( + ( + await createXPIWithID("replace@me", "2") + ).path, + xpiFile.path + ); + + await promiseInstallFail(install, ERROR_PATTERN_INSTALL_FAIL); + + equal(await AddonManager.getAddonByID("replace@me"), null); +}); + +async function do_test_update_with_file_replaced(wantPostponeTest) { + const ADDON_ID = wantPostponeTest ? "postpone@me" : "update@me"; + function backgroundWithPostpone() { + // The registration of this listener postpones the update. + browser.runtime.onUpdateAvailable.addListener(() => { + browser.test.fail("Unusable update should not call onUpdateAvailable"); + }); + } + await promiseInstallWebExtension({ + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: ADDON_ID, + update_url: `http://example.com/update-${ADDON_ID}.json`, + }, + }, + }, + background: wantPostponeTest ? backgroundWithPostpone : () => {}, + }); + + server.registerFile( + `/update-${ADDON_ID}.xpi`, + await createTempWebExtensionFile({ + manifest: { + version: "2.0", + browser_specific_settings: { gecko: { id: ADDON_ID } }, + }, + }) + ); + AddonTestUtils.registerJSON(server, `/update-${ADDON_ID}.json`, { + addons: { + [ADDON_ID]: { + updates: [ + { + version: "2.0", + update_link: `http://example.com/update-${ADDON_ID}.xpi`, + }, + ], + }, + }, + }); + + // Setup completed, let's try to verify that file corruption halts the update. + + let addon = await promiseAddonByID(ADDON_ID); + equal(addon.version, "1.0"); + + let update = await promiseFindAddonUpdates( + addon, + AddonManager.UPDATE_WHEN_USER_REQUESTED + ); + let install = update.updateAvailable; + equal(install.version, "2.0"); + equal(install.state, AddonManager.STATE_AVAILABLE); + equal(install.existingAddon, addon); + equal(install.file, null); + + let promptCount = 0; + let didReplaceFile = false; + install.promptHandler = async function () { + ++promptCount; + equal(install.state, AddonManager.STATE_DOWNLOADED); + await IOUtils.copy( + ( + await createXPIWithID(ADDON_ID, "3") + ).path, + install.file.path + ); + didReplaceFile = true; + equal(install.state, AddonManager.STATE_DOWNLOADED, "State not changed"); + }; + + if (wantPostponeTest) { + await promiseInstallFail(install, ERROR_PATTERN_POSTPONE_FAIL); + } else { + await promiseInstallFail(install, ERROR_PATTERN_INSTALL_FAIL); + } + + equal(promptCount, 1); + ok(didReplaceFile, "Replaced update with different file"); + + // Now verify that the add-on is still at the old version. + addon = await promiseAddonByID(ADDON_ID); + equal(addon.version, "1.0"); + + await addon.uninstall(); +} + +add_task(async function test_update_and_file_replaced() { + await do_test_update_with_file_replaced(); +}); + +add_task(async function test_update_postponed_and_file_replaced() { + await do_test_update_with_file_replaced(/* wantPostponeTest = */ true); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_install_icons.js b/toolkit/mozapps/extensions/test/xpcshell/test_install_icons.js new file mode 100644 index 0000000000..af88b55959 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_install_icons.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// use httpserver to find an available port +var gServer = new HttpServer(); +gServer.start(-1); +gPort = gServer.identity.primaryPort; + +var addon_url = "http://localhost:" + gPort + "/test.xpi"; +var icon32_url = "http://localhost:" + gPort + "/icon.png"; +var icon64_url = "http://localhost:" + gPort + "/icon64.png"; + +async function run_test() { + do_test_pending(); + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + await promiseStartupManager(); + + test_1(); +} + +async function test_1() { + let aInstall = await AddonManager.getInstallForURL(addon_url); + Assert.equal(aInstall.iconURL, null); + Assert.notEqual(aInstall.icons, null); + Assert.equal(aInstall.icons[32], undefined); + Assert.equal(aInstall.icons[64], undefined); + test_2(); +} + +async function test_2() { + let aInstall = await AddonManager.getInstallForURL(addon_url, { + icons: icon32_url, + }); + Assert.equal(aInstall.iconURL, icon32_url); + Assert.notEqual(aInstall.icons, null); + Assert.equal(aInstall.icons[32], icon32_url); + Assert.equal(aInstall.icons[64], undefined); + test_3(); +} + +async function test_3() { + let aInstall = await AddonManager.getInstallForURL(addon_url, { + icons: { 32: icon32_url }, + }); + Assert.equal(aInstall.iconURL, icon32_url); + Assert.notEqual(aInstall.icons, null); + Assert.equal(aInstall.icons[32], icon32_url); + Assert.equal(aInstall.icons[64], undefined); + test_4(); +} + +async function test_4() { + let aInstall = await AddonManager.getInstallForURL(addon_url, { + icons: { 32: icon32_url, 64: icon64_url }, + }); + Assert.equal(aInstall.iconURL, icon32_url); + Assert.notEqual(aInstall.icons, null); + Assert.equal(aInstall.icons[32], icon32_url); + Assert.equal(aInstall.icons[64], icon64_url); + executeSoon(do_test_finished); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_installtrigger_deprecation.js b/toolkit/mozapps/extensions/test/xpcshell/test_installtrigger_deprecation.js new file mode 100644 index 0000000000..dfaeaa44f2 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_installtrigger_deprecation.js @@ -0,0 +1,346 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42", "42"); + +const testserver = createHttpServer({ hosts: ["example.com"] }); + +function createTestPage(body) { + return `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + ${body} + </body> + </html> + `; +} + +testserver.registerPathHandler( + "/installtrigger_ua_detection.html", + (request, response) => { + response.write( + createTestPage(` + <button/> + <script> + document.querySelector("button").onclick = () => { + typeof InstallTrigger; + }; + </script> + `) + ); + } +); + +testserver.registerPathHandler( + "/installtrigger_install.html", + (request, response) => { + response.write( + createTestPage(` + <button/> + <script> + const install = InstallTrigger.install.bind(InstallTrigger); + document.querySelector("button").onclick = () => { + install({ fakeextensionurl: "http://example.com/fakeextensionurl.xpi" }); + }; + </script> + `) + ); + } +); + +async function testDeprecationWarning(testPageURL, expectedDeprecationWarning) { + const page = await ExtensionTestUtils.loadContentPage(testPageURL); + + const { message, messageInnerWindowID, pageInnerWindowID } = await page.spawn( + [expectedDeprecationWarning], + expectedWarning => { + return new Promise(resolve => { + const consoleListener = consoleMsg => { + if ( + consoleMsg instanceof Ci.nsIScriptError && + consoleMsg.message?.includes(expectedWarning) + ) { + Services.console.unregisterListener(consoleListener); + resolve({ + message: consoleMsg.message, + messageInnerWindowID: consoleMsg.innerWindowID, + pageInnerWindowID: this.content.windowGlobalChild.innerWindowId, + }); + } + }; + + Services.console.registerListener(consoleListener); + this.content.document.querySelector("button").click(); + }); + } + ); + + equal( + typeof messageInnerWindowID, + "number", + `Warning message should be associated to an innerWindowID` + ); + equal( + messageInnerWindowID, + pageInnerWindowID, + `Deprecation warning "${message}" has been logged and associated to the expected window` + ); + + await page.close(); + + return message; +} + +add_task( + { + pref_set: [ + ["extensions.InstallTrigger.enabled", true], + ["extensions.InstallTriggerImpl.enabled", true], + ], + }, + function testDeprecationWarningsOnUADetection() { + return testDeprecationWarning( + "http://example.com/installtrigger_ua_detection.html", + "InstallTrigger is deprecated and will be removed in the future." + ); + } +); + +add_task( + { + pref_set: [ + ["extensions.InstallTrigger.enabled", true], + ["extensions.InstallTriggerImpl.enabled", true], + ], + }, + async function testDeprecationWarningsOnInstallTriggerInstall() { + const message = await testDeprecationWarning( + "http://example.com/installtrigger_install.html", + "InstallTrigger.install() is deprecated and will be removed in the future." + ); + + const moreInfoURL = + "https://extensionworkshop.com/documentation/publish/self-distribution/"; + + ok( + message.includes(moreInfoURL), + "Deprecation warning should include an url to self-distribution documentation" + ); + } +); + +async function testInstallTriggerDeprecationPrefs(expectedResults) { + const page = await ExtensionTestUtils.loadContentPage("http://example.com"); + const promiseResults = page.spawn([], () => { + return { + uaDetectionResult: this.content.eval( + "typeof InstallTrigger !== 'undefined'" + ), + typeofInstallMethod: this.content.eval("typeof InstallTrigger?.install"), + }; + }); + if (expectedResults.error) { + await Assert.rejects( + promiseResults, + expectedResults.error, + "Got the expected error" + ); + } else { + Assert.deepEqual( + await promiseResults, + expectedResults, + "Got the expected results" + ); + } + await page.close(); +} + +add_task( + { + pref_set: [ + ["extensions.InstallTrigger.enabled", true], + ["extensions.InstallTriggerImpl.enabled", false], + ], + }, + function testInstallTriggerImplDisabled() { + return testInstallTriggerDeprecationPrefs({ + uaDetectionResult: true, + typeofInstallMethod: "undefined", + }); + } +); + +add_task( + { + pref_set: [["extensions.InstallTrigger.enabled", false]], + }, + function testInstallTriggerDisabled() { + return testInstallTriggerDeprecationPrefs({ + error: /ReferenceError: InstallTrigger is not defined/, + }); + } +); + +add_task( + { + pref_set: [ + ["extensions.remoteSettings.disabled", false], + ["extensions.InstallTrigger.enabled", true], + ["extensions.InstallTriggerImpl.enabled", true], + ], + }, + async function testInstallTriggerDeprecatedFromRemoteSettings() { + await AddonTestUtils.promiseStartupManager(); + + // InstallTrigger is expected to be initially enabled. + await testInstallTriggerDeprecationPrefs({ + uaDetectionResult: true, + typeofInstallMethod: "function", + }); + + info("Test remote settings update to hide InstallTrigger methods"); + + // InstallTrigger global is expected to still be enabled, the install method + // to have been hidden. + const unexpectedPrefsBranchName = "extensions.unexpectedPrefs"; + await setAndEmitFakeRemoteSettingsData([ + { + id: "AddonManagerSettings", + installTriggerDeprecation: { + "extensions.InstallTriggerImpl.enabled": false, + // Unexpected preferences names would be just ignored. + [`${unexpectedPrefsBranchName}.fromProcessedEntry`]: true, + }, + otherFakeFutureSetting: { + [`${unexpectedPrefsBranchName}.fromFakeFutureSetting`]: true, + }, + // This entry is expected to always be processed when running this + // xpcshell test, the appInfo platformVersion is always set to 42 + // by the call to AddonTestUtils's createAppInfo. + filter_expression: "env.appinfo.platformVersion >= 42", + }, + { + // Entries entirely unexpected should be ignored even if they may be + // including a property named as the ones that AMRemoteSettings (e.g. + // it may be a new type of entry introduced for a new Firefox version, + // which a previous version of Firefox shouldn't try to process avoid + // undefined behaviors). + id: "AddonManagerSettings-fxFutureVersion", + // This entry is expected to always be filtered out by RemoteSettings, + // while running this xpcshell test the platformInfo version is always set + // to 42 by the call to AddonTestUtils's createAppInfo. + filter_expression: "env.appinfo.platformVersion >= 200", + installTriggerDeprecation: { + // If processed, it would fail the assertion that follows + // because it does change the same pref that the previous entry did + // set to false. + "extensions.InstallTriggerImpl.enabled": true, + }, + }, + ]); + await testInstallTriggerDeprecationPrefs({ + uaDetectionResult: true, + typeofInstallMethod: "undefined", + }); + + const unexpectedPrefBranch = Services.prefs.getBranch( + unexpectedPrefsBranchName + ); + equal( + unexpectedPrefBranch.getPrefType("fromFakeFutureSetting"), + unexpectedPrefBranch.PREF_INVALID, + "Preferences included in an unexpected entry property should not be set" + ); + equal( + unexpectedPrefBranch.getPrefType("fromProcessedEntry"), + unexpectedPrefBranch.PREF_INVALID, + undefined, + "Unexpected pref included in the installTriggerDeprecation entry should not be set" + ); + + info("Test remote settings update to hide InstallTrigger global"); + // InstallTrigger global is expected to still be enabled, the install method + // to have been hidden. + await setAndEmitFakeRemoteSettingsData([ + { + id: "AddonManagerSettings", + installTriggerDeprecation: { + "extensions.InstallTrigger.enabled": false, + }, + }, + ]); + await testInstallTriggerDeprecationPrefs({ + error: /ReferenceError: InstallTrigger is not defined/, + }); + + info("Test remote settings update to re-enable InstallTrigger global"); + // InstallTrigger global is expected to still be enabled, the install method + // to have been hidden. + await setAndEmitFakeRemoteSettingsData([ + { + id: "AddonManagerSettings", + installTriggerDeprecation: { + "extensions.InstallTrigger.enabled": true, + "extensions.InstallTriggerImpl.enabled": false, + }, + }, + ]); + await testInstallTriggerDeprecationPrefs({ + uaDetectionResult: true, + typeofInstallMethod: "undefined", + }); + + info("Test remote settings update to re-enable InstallTrigger methods"); + // InstallTrigger global and method are both expected to be re-enabled. + await setAndEmitFakeRemoteSettingsData([ + { + id: "AddonManagerSettings", + installTriggerDeprecation: { + "extensions.InstallTrigger.enabled": true, + "extensions.InstallTriggerImpl.enabled": true, + }, + }, + ]); + await testInstallTriggerDeprecationPrefs({ + uaDetectionResult: true, + typeofInstallMethod: "function", + }); + + info("Test remote settings ignored when AMRemoteSettings is disabled"); + // RemoteSettings are expected to be ignored. + Services.prefs.setBoolPref("extensions.remoteSettings.disabled", true); + await setAndEmitFakeRemoteSettingsData( + [ + { + id: "AddonManagerSettings", + installTriggerDeprecation: { + "extensions.InstallTrigger.enabled": false, + "extensions.InstallTriggerImpl.enabled": false, + }, + }, + ], + false /* expectClientInitialized */ + ); + await testInstallTriggerDeprecationPrefs({ + uaDetectionResult: true, + typeofInstallMethod: "function", + }); + + info( + "Test previously synchronized are processed on AOM started when AMRemoteSettings are enabled" + ); + // RemoteSettings previously stored on disk are expected to disable InstallTrigger global and methods. + await AddonTestUtils.promiseShutdownManager(); + Services.prefs.setBoolPref("extensions.remoteSettings.disabled", false); + await AddonTestUtils.promiseStartupManager(); + await testInstallTriggerDeprecationPrefs({ + error: /ReferenceError: InstallTrigger is not defined/, + }); + + await AddonTestUtils.promiseShutdownManager(); + } +); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_installtrigger_schemes.js b/toolkit/mozapps/extensions/test/xpcshell/test_installtrigger_schemes.js new file mode 100644 index 0000000000..b219d2f55d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_installtrigger_schemes.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +createHttpServer({ hosts: ["example.com"] }); + +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "1.9.2" +); + +async function assertInstallTriggetRejected(page, xpi_url, expectedError) { + await Assert.rejects( + page.spawn([xpi_url], async url => { + this.content.eval(`InstallTrigger.install({extension: '${url}'});`); + }), + expectedError, + `InstallTrigger.install expected to throw on xpi url "${xpi_url}"` + ); +} + +add_task( + { + // Once InstallTrigger is removed, this test should be removed as well. + pref_set: [ + ["extensions.InstallTrigger.enabled", true], + ["extensions.InstallTriggerImpl.enabled", true], + // Relax the user input requirements while running this test. + ["xpinstall.userActivation.required", false], + ], + }, + async function test_InstallTriggerThrows_on_unsupported_xpi_schemes_blob() { + const page = await ExtensionTestUtils.loadContentPage("http://example.com"); + const blob_url = await page.spawn([], () => { + return this.content.eval(`(function () { + const blob = new Blob(['fakexpicontent']); + return URL.createObjectURL(blob); + })()`); + }); + await assertInstallTriggetRejected(page, blob_url, /Unsupported scheme/); + await page.close(); + } +); + +add_task( + { + // Once InstallTrigger is removed, this test should be removed as well. + pref_set: [ + ["extensions.InstallTrigger.enabled", true], + ["extensions.InstallTriggerImpl.enabled", true], + // Relax the user input requirements while running this test. + ["xpinstall.userActivation.required", false], + ], + }, + async function test_InstallTriggerThrows_on_unsupported_xpi_schemes_data() { + const page = await ExtensionTestUtils.loadContentPage("http://example.com"); + const data_url = "data:;,fakexpicontent"; + // This is actually rejected by the checkLoadURIWithPrincipal, which fails with + // NS_ERROR_DOM_BAD_URI triggered by CheckLoadURIWithPrincipal's call to + // + // DenyAccessIfURIHasFlags(aTargetURI, nsIProtocolHandler::URI_INHERITS_SECURITY_CONTEXT) + // + // and so it is not a site permission that the user can actually grant, unlike the error + // raised may suggest. + await assertInstallTriggetRejected( + page, + data_url, + /Insufficient permissions to install/ + ); + await page.close(); + } +); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_isDebuggable.js b/toolkit/mozapps/extensions/test/xpcshell/test_isDebuggable.js new file mode 100644 index 0000000000..b23e94e7bb --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_isDebuggable.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +var ID = "debuggable@tests.mozilla.org"; + +add_task(async function () { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2"); + + await promiseStartupManager(); + + await promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { gecko: { id: ID } }, + }, + }); + + let addon = await AddonManager.getAddonByID(ID); + Assert.equal(addon.isDebuggable, true); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_isReady.js b/toolkit/mozapps/extensions/test/xpcshell/test_isReady.js new file mode 100644 index 0000000000..e5e1649051 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_isReady.js @@ -0,0 +1,71 @@ +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + +add_task(async function () { + equal(AddonManager.isReady, false, "isReady should be false before startup"); + + let gotStartupEvent = false; + let gotShutdownEvent = false; + let listener = { + onStartup() { + gotStartupEvent = true; + }, + onShutdown() { + gotShutdownEvent = true; + }, + }; + AddonManager.addManagerListener(listener); + + info("Starting manager..."); + await promiseStartupManager(); + equal(AddonManager.isReady, true, "isReady should be true after startup"); + equal( + gotStartupEvent, + true, + "Should have seen onStartup event after startup" + ); + equal( + gotShutdownEvent, + false, + "Should not have seen onShutdown event before shutdown" + ); + + gotStartupEvent = false; + gotShutdownEvent = false; + + info("Shutting down manager..."); + await promiseShutdownManager(); + + equal(AddonManager.isReady, false, "isReady should be false after shutdown"); + equal( + gotStartupEvent, + false, + "Should not have seen onStartup event after shutdown" + ); + equal( + gotShutdownEvent, + true, + "Should have seen onShutdown event after shutdown" + ); + + AddonManager.addManagerListener(listener); + gotStartupEvent = false; + gotShutdownEvent = false; + + info("Starting manager again..."); + await promiseStartupManager(); + equal( + AddonManager.isReady, + true, + "isReady should be true after repeat startup" + ); + equal( + gotStartupEvent, + true, + "Should have seen onStartup event after repeat startup" + ); + equal( + gotShutdownEvent, + false, + "Should not have seen onShutdown event before shutdown, following repeat startup" + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_loadManifest_isPrivileged.js b/toolkit/mozapps/extensions/test/xpcshell/test_loadManifest_isPrivileged.js new file mode 100644 index 0000000000..2e8190d57b --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_loadManifest_isPrivileged.js @@ -0,0 +1,233 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { XPIExports } = ChromeUtils.importESModule( + "resource://gre/modules/addons/XPIExports.sys.mjs" +); + +// NOTE: Only constants can be extracted from XPIExports. +const { + XPIInternal: { + KEY_APP_PROFILE, + KEY_APP_SYSTEM_DEFAULTS, + KEY_APP_SYSTEM_PROFILE, + }, +} = XPIExports; + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); + +// Disable "xpc::IsInAutomation()", since it would override the behavior +// we're testing for. +Services.prefs.setBoolPref( + "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer", + false +); + +Services.prefs.setIntPref( + "extensions.enabledScopes", + // SCOPE_PROFILE is enabled by default, + // SCOPE_APPLICATION is to enable KEY_APP_SYSTEM_PROFILE, which we need to + // test the combination (isSystem && !isBuiltin) in test_system_location. + AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION +); +// test_builtin_system_location tests the (isSystem && isBuiltin) combination +// (i.e. KEY_APP_SYSTEM_DEFAULTS). That location only exists if this directory +// is found: +const distroDir = FileUtils.getDir("ProfD", ["sysfeatures"]); +distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", distroDir); + +function getInstallLocation({ + isBuiltin = false, + isSystem = false, + isTemporary = false, +}) { + if (isTemporary) { + // Temporary installation. Signatures will not be verified. + return XPIExports.XPIInternal.TemporaryInstallLocation; // KEY_APP_TEMPORARY + } + let location; + if (isSystem) { + if (isBuiltin) { + // System location. Signatures will not be verified. + location = XPIExports.XPIInternal.XPIStates.getLocation( + KEY_APP_SYSTEM_DEFAULTS + ); + } else { + // Normandy installations. Signatures will be verified. + location = XPIExports.XPIInternal.XPIStates.getLocation( + KEY_APP_SYSTEM_PROFILE + ); + } + } else if (isBuiltin) { + // Packaged with the application. Signatures will not be verified. + location = XPIExports.XPIInternal.BuiltInLocation; // KEY_APP_BUILTINS + } else { + // By default - The profile directory. Signatures will be verified. + location = XPIExports.XPIInternal.XPIStates.getLocation(KEY_APP_PROFILE); + } + // Sanity checks to make sure that the flags match the expected values. + if (location.isSystem !== isSystem) { + ok(false, `${location.name}, unexpected isSystem=${location.isSystem}`); + } + if (location.isBuiltin !== isBuiltin) { + ok(false, `${location.name}, unexpected isBuiltin=${location.isBuiltin}`); + } + return location; +} + +async function testLoadManifest({ location, expectPrivileged }) { + location ??= getInstallLocation({}); + let xpi = await AddonTestUtils.createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { gecko: { id: "@with-privileged-perm" } }, + permissions: ["mozillaAddons", "cookies"], + }, + }); + let actualPermissions; + let { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { + if (location.isTemporary && !expectPrivileged) { + ExtensionTestUtils.failOnSchemaWarnings(false); + await Assert.rejects( + XPIExports.XPIInstall.loadManifestFromFile(xpi, location), + /Extension is invalid/, + "load manifest failed with privileged permission" + ); + ExtensionTestUtils.failOnSchemaWarnings(true); + return; + } + let addon = await XPIExports.XPIInstall.loadManifestFromFile(xpi, location); + actualPermissions = addon.userPermissions; + equal(addon.isPrivileged, expectPrivileged, "addon.isPrivileged"); + }); + if (expectPrivileged) { + AddonTestUtils.checkMessages(messages, { + expected: [], + forbidden: [ + { + message: /Reading manifest: Invalid extension permission/, + }, + ], + }); + Assert.deepEqual( + actualPermissions, + { origins: [], permissions: ["mozillaAddons", "cookies"] }, + "Privileged permission should exist" + ); + } else if (location.isTemporary) { + AddonTestUtils.checkMessages(messages, { + expected: [ + { + message: + /Using the privileged permission 'mozillaAddons' requires a privileged add-on/, + }, + ], + forbidden: [], + }); + } else { + AddonTestUtils.checkMessages(messages, { + expected: [ + { + message: + /Reading manifest: Invalid extension permission: mozillaAddons/, + }, + ], + forbidden: [], + }); + Assert.deepEqual( + actualPermissions, + { origins: [], permissions: ["cookies"] }, + "Privileged permission should be ignored" + ); + } +} + +add_task(async function setup() { + await ExtensionTestUtils.startAddonManager(); +}); + +add_task(async function test_regular_addon() { + AddonTestUtils.usePrivilegedSignatures = false; + await testLoadManifest({ + expectPrivileged: false, + }); +}); + +add_task(async function test_privileged_signature() { + AddonTestUtils.usePrivilegedSignatures = true; + await testLoadManifest({ + expectPrivileged: true, + }); +}); + +add_task(async function test_system_signature() { + AddonTestUtils.usePrivilegedSignatures = "system"; + await testLoadManifest({ + expectPrivileged: true, + }); +}); + +add_task(async function test_builtin_location() { + AddonTestUtils.usePrivilegedSignatures = false; + await testLoadManifest({ + expectPrivileged: true, + location: getInstallLocation({ isBuiltin: true }), + }); +}); + +add_task(async function test_system_location() { + AddonTestUtils.usePrivilegedSignatures = false; + await testLoadManifest({ + expectPrivileged: false, + location: getInstallLocation({ isSystem: true }), + }); +}); + +add_task(async function test_builtin_system_location() { + AddonTestUtils.usePrivilegedSignatures = false; + await testLoadManifest({ + expectPrivileged: true, + location: getInstallLocation({ isSystem: true, isBuiltin: true }), + }); +}); + +add_task(async function test_temporary_regular() { + AddonTestUtils.usePrivilegedSignatures = false; + Services.prefs.setBoolPref("extensions.experiments.enabled", false); + await testLoadManifest({ + expectPrivileged: false, + location: getInstallLocation({ isTemporary: true }), + }); +}); + +add_task(async function test_temporary_privileged_signature() { + AddonTestUtils.usePrivilegedSignatures = true; + Services.prefs.setBoolPref("extensions.experiments.enabled", false); + await testLoadManifest({ + expectPrivileged: true, + location: getInstallLocation({ isTemporary: true }), + }); +}); + +add_task(async function test_temporary_experiments_enabled() { + AddonTestUtils.usePrivilegedSignatures = false; + Services.prefs.setBoolPref("extensions.experiments.enabled", true); + + // Experiments can only be used if AddonSettings.EXPERIMENTS_ENABLED is true. + // This is the condition behind the flag, minus Cu.isInAutomation. Currently + // that flag is false despite this being a test (see bug 1598804), but that + // is desired in this case because we want the test to confirm the real-world + // behavior instead of test-specific behavior. + const areTemporaryExperimentsAllowed = + !AppConstants.MOZ_REQUIRE_SIGNING || + AppConstants.NIGHTLY_BUILD || + AppConstants.MOZ_DEV_EDITION; + + await testLoadManifest({ + expectPrivileged: areTemporaryExperimentsAllowed, + location: getInstallLocation({ isTemporary: true }), + }); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_locale.js b/toolkit/mozapps/extensions/test/xpcshell/test_locale.js new file mode 100644 index 0000000000..2824c489b4 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_locale.js @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +add_task(async function setup() { + // Setup for test + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + await promiseStartupManager(); +}); + +// Tests that the localized properties are visible before installation +add_task(async function test_1() { + await restartWithLocales(["fr-FR"]); + + let xpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + name: "__MSG_name__", + description: "__MSG_description__", + default_locale: "en", + + browser_specific_settings: { + gecko: { + id: "addon1@tests.mozilla.org", + }, + }, + }, + + files: { + "_locales/en/messages.json": { + name: { + message: "Fallback Name", + description: "name", + }, + description: { + message: "Fallback Description", + description: "description", + }, + }, + "_locales/fr_FR/messages.json": { + name: { + message: "fr-FR Name", + description: "name", + }, + description: { + message: "fr-FR Description", + description: "description", + }, + }, + "_locales/de-DE/messages.json": { + name: { + message: "de-DE Name", + description: "name", + }, + }, + }, + }); + + let install = await AddonManager.getInstallForFile(xpi); + Assert.equal(install.addon.name, "fr-FR Name"); + Assert.equal(install.addon.description, "fr-FR Description"); + await install.install(); +}); + +// Tests that the localized properties are visible after installation +add_task(async function test_2() { + let addon = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + Assert.notEqual(addon, null); + + Assert.equal(addon.name, "fr-FR Name"); + Assert.equal(addon.description, "fr-FR Description"); + + await addon.disable(); +}); + +// Test that the localized properties are still there when disabled. +add_task(async function test_3() { + let addon = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + Assert.notEqual(addon, null); + Assert.equal(addon.name, "fr-FR Name"); +}); + +// Test that changing locale works +add_task(async function test_5() { + await restartWithLocales(["de-DE"]); + + let addon = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + Assert.notEqual(addon, null); + + Assert.equal(addon.name, "de-DE Name"); + Assert.equal(addon.description, "Fallback Description"); +}); + +// Test that missing locales use the fallbacks +add_task(async function test_6() { + await restartWithLocales(["nl-NL"]); + + let addon = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + Assert.notEqual(addon, null); + + Assert.equal(addon.name, "Fallback Name"); + Assert.equal(addon.description, "Fallback Description"); + + await addon.enable(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_moved_extension_metadata.js b/toolkit/mozapps/extensions/test/xpcshell/test_moved_extension_metadata.js new file mode 100644 index 0000000000..13ac8a1e57 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_moved_extension_metadata.js @@ -0,0 +1,186 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This test is disabled but is being kept around so it can eventualy +// be modernized and re-enabled. But is uses obsolete test helpers that +// fail lint, so just skip linting it for now. +/* eslint-disable */ + +// This verifies that moving an extension in the filesystem without any other +// change still keeps updated compatibility information + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false); +// Enable loading extensions from the user and system scopes +Services.prefs.setIntPref( + "extensions.enabledScopes", + AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_USER +); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "1.9.2"); + +var testserver = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); +testserver.registerDirectory("/data/", do_get_file("data")); + +var userDir = gProfD.clone(); +userDir.append("extensions2"); +userDir.append(gAppInfo.ID); + +var dirProvider = { + getFile(aProp, aPersistent) { + aPersistent.value = false; + if (aProp == "XREUSysExt") return userDir.parent; + return null; + }, + + QueryInterface: ChromeUtils.generateQI(["nsIDirectoryServiceProvider"]), +}; +Services.dirsvc.registerProvider(dirProvider); + +var addon1 = { + id: "addon1@tests.mozilla.org", + version: "1.0", + name: "Test 1", + bootstrap: true, + updateURL: "http://example.com/data/test_bug655254.json", + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1", + }, + ], +}; + +const ADDONS = [ + { + "install.rdf": { + id: "addon2@tests.mozilla.org", + name: "Test 2", + + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "2", + maxVersion: "2", + }, + ], + }, + "bootstrap.js": ` + /* exported startup, shutdown */ + function startup(data, reason) { + Services.prefs.setIntPref("bootstraptest.active_version", 1); + } + + function shutdown(data, reason) { + Services.prefs.setIntPref("bootstraptest.active_version", 0); + } + `, + }, +]; + +const XPIS = ADDONS.map(addon => AddonTestUtils.createTempXPIFile(addon)); + +add_task(async function test_1() { + var time = Date.now(); + var dir = await promiseWriteInstallRDFForExtension(addon1, userDir); + setExtensionModifiedTime(dir, time); + + await manuallyInstall(XPIS[0], userDir, "addon2@tests.mozilla.org"); + + await promiseStartupManager(); + + let [a1, a2] = await AddonManager.getAddonsByIDs([ + "addon1@tests.mozilla.org", + "addon2@tests.mozilla.org", + ]); + Assert.notEqual(a1, null); + Assert.ok(a1.appDisabled); + Assert.ok(!a1.isActive); + Assert.ok(!isExtensionInBootstrappedList(userDir, a1.id)); + + Assert.notEqual(a2, null); + Assert.ok(!a2.appDisabled); + Assert.ok(a2.isActive); + Assert.ok(isExtensionInBootstrappedList(userDir, a2.id)); + Assert.equal(Services.prefs.getIntPref("bootstraptest.active_version"), 1); + + await AddonTestUtils.promiseFindAddonUpdates( + a1, + AddonManager.UPDATE_WHEN_USER_REQUESTED + ); + + await promiseRestartManager(); + + let a1_2 = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + Assert.notEqual(a1_2, null); + Assert.ok(!a1_2.appDisabled); + Assert.ok(a1_2.isActive); + Assert.ok(isExtensionInBootstrappedList(userDir, a1_2.id)); + + await promiseShutdownManager(); + + Assert.equal(Services.prefs.getIntPref("bootstraptest.active_version"), 0); + + userDir.parent.moveTo(gProfD, "extensions3"); + userDir = gProfD.clone(); + userDir.append("extensions3"); + userDir.append(gAppInfo.ID); + Assert.ok(userDir.exists()); + + await promiseStartupManager(); + + let [a1_3, a2_3] = await AddonManager.getAddonsByIDs([ + "addon1@tests.mozilla.org", + "addon2@tests.mozilla.org", + ]); + Assert.notEqual(a1_3, null); + Assert.ok(!a1_3.appDisabled); + Assert.ok(a1_3.isActive); + Assert.ok(isExtensionInBootstrappedList(userDir, a1_3.id)); + + Assert.notEqual(a2_3, null); + Assert.ok(!a2_3.appDisabled); + Assert.ok(a2_3.isActive); + Assert.ok(isExtensionInBootstrappedList(userDir, a2_3.id)); + Assert.equal(Services.prefs.getIntPref("bootstraptest.active_version"), 1); +}); + +// Set up the profile +add_task(async function test_2() { + let a2 = await AddonManager.getAddonByID("addon2@tests.mozilla.org"); + Assert.notEqual(a2, null); + Assert.ok(!a2.appDisabled); + Assert.ok(a2.isActive); + Assert.ok(isExtensionInBootstrappedList(userDir, a2.id)); + Assert.equal(Services.prefs.getIntPref("bootstraptest.active_version"), 1); + + await a2.disable(); + Assert.equal(Services.prefs.getIntPref("bootstraptest.active_version"), 0); + + await promiseShutdownManager(); + + userDir.parent.moveTo(gProfD, "extensions4"); + userDir = gProfD.clone(); + userDir.append("extensions4"); + userDir.append(gAppInfo.ID); + Assert.ok(userDir.exists()); + + await promiseStartupManager(); + + let [a1_2, a2_2] = await AddonManager.getAddonsByIDs([ + "addon1@tests.mozilla.org", + "addon2@tests.mozilla.org", + ]); + Assert.notEqual(a1_2, null); + Assert.ok(!a1_2.appDisabled); + Assert.ok(a1_2.isActive); + Assert.ok(isExtensionInBootstrappedList(userDir, a1_2.id)); + + Assert.notEqual(a2_2, null); + Assert.ok(a2_2.userDisabled); + Assert.ok(!a2_2.isActive); + Assert.ok(!isExtensionInBootstrappedList(userDir, a2_2.id)); + Assert.equal(Services.prefs.getIntPref("bootstraptest.active_version"), 0); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_no_addons.js b/toolkit/mozapps/extensions/test/xpcshell/test_no_addons.js new file mode 100644 index 0000000000..14ac8a2c17 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_no_addons.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test startup and restart when no add-ons are installed +// bug 944006 + +// Load XPI Provider to get schema version ID +const { + XPIExports: { + XPIInternal: { DB_SCHEMA }, + }, +} = ChromeUtils.importESModule( + "resource://gre/modules/addons/XPIExports.sys.mjs" +); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + +// Test for a preference to either exist with a specified value, or not exist at all +function checkPending() { + try { + Assert.ok(!Services.prefs.getBoolPref("extensions.pendingOperations")); + } catch (e) { + // OK + } +} + +// Make sure all our extension state is empty/nonexistent +function check_empty_state() { + Assert.equal( + Services.prefs.getIntPref("extensions.databaseSchema"), + DB_SCHEMA + ); + checkPending(); +} + +// After first run with no add-ons, we expect: +// no extensions.json is created +// no extensions.ini +// database schema version preference is set +// bootstrap add-ons preference is not found +// add-on directory state preference is an empty array +// no pending operations +add_task(async function first_run() { + await promiseStartupManager(); + check_empty_state(); + await true; +}); + +// Now do something that causes a DB load, and re-check +async function trigger_db_load() { + let addonList = await AddonManager.getAddonsByTypes(["extension"]); + + Assert.equal(addonList.length, 0); + check_empty_state(); + + await true; +} +add_task(trigger_db_load); + +// Now restart the manager and check again +add_task(async function restart_and_recheck() { + await promiseRestartManager(); + check_empty_state(); + await true; +}); + +// and reload the DB again +add_task(trigger_db_load); + +// When we start up with no DB and an old database schema, we should update the +// schema number but not create a database +add_task(async function upgrade_schema_version() { + await promiseShutdownManager(); + Services.prefs.setIntPref("extensions.databaseSchema", 1); + + await promiseStartupManager(); + Assert.equal( + Services.prefs.getIntPref("extensions.databaseSchema"), + DB_SCHEMA + ); + check_empty_state(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_nodisable_hidden.js b/toolkit/mozapps/extensions/test/xpcshell/test_nodisable_hidden.js new file mode 100644 index 0000000000..e9f1a6626f --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_nodisable_hidden.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This test verifies that hidden add-ons cannot be user disabled. + +// for system add-ons +const distroDir = FileUtils.getDir("ProfD", ["sysfeatures"]); +distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", distroDir); + +const NORMAL_ID = "normal@tests.mozilla.org"; +const SYSTEM_ID = "system@tests.mozilla.org"; + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +// normal add-ons can be user disabled. +add_task(async function () { + await promiseStartupManager(); + + await promiseInstallWebExtension({ + manifest: { + name: "Test disabling hidden add-ons, non-hidden add-on case.", + version: "1.0", + browser_specific_settings: { gecko: { id: NORMAL_ID } }, + }, + }); + + let addon = await promiseAddonByID(NORMAL_ID); + Assert.notEqual(addon, null); + Assert.equal(addon.version, "1.0"); + Assert.equal( + addon.name, + "Test disabling hidden add-ons, non-hidden add-on case." + ); + Assert.ok(addon.isCompatible); + Assert.ok(!addon.appDisabled); + Assert.ok(!addon.userDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.type, "extension"); + + // normal add-ons can be disabled by the user. + await addon.disable(); + + Assert.notEqual(addon, null); + Assert.equal(addon.version, "1.0"); + Assert.equal( + addon.name, + "Test disabling hidden add-ons, non-hidden add-on case." + ); + Assert.ok(addon.isCompatible); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.userDisabled); + Assert.ok(!addon.isActive); + Assert.equal(addon.type, "extension"); + + await addon.uninstall(); + + await promiseShutdownManager(); +}); + +// system add-ons can never be user disabled. +add_task(async function () { + let xpi = createTempWebExtensionFile({ + manifest: { + name: "Test disabling hidden add-ons, hidden system add-on case.", + version: "1.0", + browser_specific_settings: { gecko: { id: SYSTEM_ID } }, + }, + }); + xpi.copyTo(distroDir, `${SYSTEM_ID}.xpi`); + await overrideBuiltIns({ system: [SYSTEM_ID] }); + + await promiseStartupManager(); + + let addon = await promiseAddonByID(SYSTEM_ID); + Assert.notEqual(addon, null); + Assert.equal(addon.version, "1.0"); + Assert.equal( + addon.name, + "Test disabling hidden add-ons, hidden system add-on case." + ); + Assert.ok(addon.isCompatible); + Assert.ok(!addon.appDisabled); + Assert.ok(!addon.userDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.type, "extension"); + + // system add-ons cannot be disabled by the user. + await Assert.rejects( + addon.disable(), + err => err.message == `Cannot disable system add-on ${SYSTEM_ID}`, + "disable() on a hidden add-on should fail" + ); + + Assert.ok(!addon.userDisabled); + Assert.ok(addon.isActive); + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_onPropertyChanged_appDisabled.js b/toolkit/mozapps/extensions/test/xpcshell/test_onPropertyChanged_appDisabled.js new file mode 100644 index 0000000000..7a7cd6543e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_onPropertyChanged_appDisabled.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const ID = "addon1@tests.mozilla.org"; +add_task(async function run_test() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + + let xpi = createAddon({ + id: ID, + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "0.1", + maxVersion: "0.2", + }, + ], + }); + await manuallyInstall(xpi, AddonTestUtils.profileExtensions, ID); + + AddonManager.strictCompatibility = false; + await promiseStartupManager(); + + let addon = await AddonManager.getAddonByID(ID); + Assert.notEqual(addon, null); + await addon.disable(); + + Assert.ok(addon.userDisabled); + Assert.ok(!addon.isActive); + Assert.ok(!addon.appDisabled); + + let promise = promiseAddonEvent("onPropertyChanged"); + AddonManager.strictCompatibility = true; + let [, properties] = await promise; + + Assert.deepEqual( + properties, + ["appDisabled"], + "Got onPropertyChanged for appDisabled" + ); + Assert.ok(addon.appDisabled); + + promise = promiseAddonEvent("onPropertyChanged"); + AddonManager.strictCompatibility = false; + [, properties] = await promise; + + Assert.deepEqual( + properties, + ["appDisabled"], + "Got onPropertyChanged for appDisabled" + ); + Assert.ok(!addon.appDisabled); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_permissions.js b/toolkit/mozapps/extensions/test/xpcshell/test_permissions.js new file mode 100644 index 0000000000..30c9aa92b0 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_permissions.js @@ -0,0 +1,199 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Checks that permissions set in preferences are correctly imported but can +// be removed by the user. + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +const XPI_MIMETYPE = "application/x-xpinstall"; + +function newPrincipal(uri) { + return Services.scriptSecurityManager.createContentPrincipal( + NetUtil.newURI(uri), + {} + ); +} + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2"); + + Services.prefs.setCharPref( + "xpinstall.whitelist.add", + "https://test1.com,https://test2.com" + ); + Services.prefs.setCharPref( + "xpinstall.whitelist.add.36", + "https://test3.com,https://www.test4.com" + ); + Services.prefs.setCharPref( + "xpinstall.whitelist.add.test5", + "https://test5.com" + ); + + PermissionTestUtils.add( + "https://www.test9.com", + "install", + Ci.nsIPermissionManager.ALLOW_ACTION + ); + + await promiseStartupManager(); + + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("http://test1.com") + ) + ); + Assert.ok( + AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://test1.com") + ) + ); + Assert.ok( + AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://www.test2.com") + ) + ); + Assert.ok( + AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://test3.com") + ) + ); + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://test4.com") + ) + ); + Assert.ok( + AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://www.test4.com") + ) + ); + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("http://www.test5.com") + ) + ); + Assert.ok( + AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://www.test5.com") + ) + ); + + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("http://www.test6.com") + ) + ); + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://www.test6.com") + ) + ); + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://test7.com") + ) + ); + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://www.test8.com") + ) + ); + + // This should remain unaffected + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("http://www.test9.com") + ) + ); + Assert.ok( + AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://www.test9.com") + ) + ); + + Services.perms.removeAll(); + + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://test1.com") + ) + ); + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://www.test2.com") + ) + ); + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://test3.com") + ) + ); + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://www.test4.com") + ) + ); + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://www.test5.com") + ) + ); + + // Upgrade the application and verify that the permissions are still not there + await promiseRestartManager("2"); + + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://test1.com") + ) + ); + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://www.test2.com") + ) + ); + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://test3.com") + ) + ); + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://www.test4.com") + ) + ); + Assert.ok( + !AddonManager.isInstallAllowed( + XPI_MIMETYPE, + newPrincipal("https://www.test5.com") + ) + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_permissions_prefs.js b/toolkit/mozapps/extensions/test/xpcshell/test_permissions_prefs.js new file mode 100644 index 0000000000..d7bcaa038c --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_permissions_prefs.js @@ -0,0 +1,99 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that xpinstall.[whitelist|blacklist].add preferences are emptied when +// converted into permissions. + +const PREF_XPI_WHITELIST_PERMISSIONS = "xpinstall.whitelist.add"; +const PREF_XPI_BLACKLIST_PERMISSIONS = "xpinstall.blacklist.add"; + +const { PermissionsTestUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PermissionsUtils.sys.mjs" +); + +function newPrincipal(uri) { + return Services.scriptSecurityManager.createContentPrincipal( + NetUtil.newURI(uri), + {} + ); +} + +function do_check_permission_prefs(preferences) { + // Check preferences were emptied + for (let pref of preferences) { + try { + Assert.equal(Services.prefs.getCharPref(pref), ""); + } catch (e) { + // Successfully emptied + } + } +} + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9"); + + // Create own preferences to test + Services.prefs.setCharPref("xpinstall.whitelist.add.EMPTY", ""); + Services.prefs.setCharPref( + "xpinstall.whitelist.add.TEST", + "http://whitelist.example.com" + ); + Services.prefs.setCharPref("xpinstall.blacklist.add.EMPTY", ""); + Services.prefs.setCharPref( + "xpinstall.blacklist.add.TEST", + "http://blacklist.example.com" + ); + + // Get list of preferences to check + var whitelistPreferences = Services.prefs.getChildList( + PREF_XPI_WHITELIST_PERMISSIONS + ); + var blacklistPreferences = Services.prefs.getChildList( + PREF_XPI_BLACKLIST_PERMISSIONS + ); + var preferences = whitelistPreferences.concat(blacklistPreferences); + + await promiseStartupManager(); + + // Permissions are imported lazily - act as thought we're checking an install, + // to trigger on-deman importing of the permissions. + AddonManager.isInstallAllowed( + "application/x-xpinstall", + newPrincipal("http://example.com/file.xpi") + ); + do_check_permission_prefs(preferences); + + // Import can also be triggered by an observer notification by any other area + // of code, such as a permissions management UI. + + // First, request to flush all permissions + PermissionsTestUtils.clearImportedPrefBranches(); + Services.prefs.setCharPref( + "xpinstall.whitelist.add.TEST2", + "https://whitelist2.example.com" + ); + Services.obs.notifyObservers(null, "flush-pending-permissions", "install"); + do_check_permission_prefs(preferences); + + // Then, request to flush just install permissions + PermissionsTestUtils.clearImportedPrefBranches(); + Services.prefs.setCharPref( + "xpinstall.whitelist.add.TEST3", + "https://whitelist3.example.com" + ); + Services.obs.notifyObservers(null, "flush-pending-permissions"); + do_check_permission_prefs(preferences); + + // And a request to flush some other permissions sholdn't flush install permissions + PermissionsTestUtils.clearImportedPrefBranches(); + Services.prefs.setCharPref( + "xpinstall.whitelist.add.TEST4", + "https://whitelist4.example.com" + ); + Services.obs.notifyObservers(null, "flush-pending-permissions", "lolcats"); + Assert.equal( + Services.prefs.getCharPref("xpinstall.whitelist.add.TEST4"), + "https://whitelist4.example.com" + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_pref_properties.js b/toolkit/mozapps/extensions/test/xpcshell/test_pref_properties.js new file mode 100644 index 0000000000..cb816dcd8d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_pref_properties.js @@ -0,0 +1,221 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the preference-related properties of AddonManager +// eg: AddonManager.checkCompatibility, AddonManager.updateEnabled, etc + +var gManagerEventsListener = { + seenEvents: [], + init() { + let events = [ + "onCompatibilityModeChanged", + "onCheckUpdateSecurityChanged", + "onUpdateModeChanged", + ]; + events.forEach(function (aEvent) { + this[aEvent] = function () { + info("Saw event " + aEvent); + this.seenEvents.push(aEvent); + }; + }, this); + AddonManager.addManagerListener(this); + // Try to add twice, to test that the second time silently fails. + AddonManager.addManagerListener(this); + }, + shutdown() { + AddonManager.removeManagerListener(this); + }, + expect(aEvents) { + this.expectedEvents = aEvents; + }, + checkExpected() { + info("Checking expected events..."); + while (this.expectedEvents.length) { + let event = this.expectedEvents.pop(); + info("Looking for expected event " + event); + let matchingEvents = this.seenEvents.filter(function (aSeenEvent) { + return aSeenEvent == event; + }); + Assert.equal(matchingEvents.length, 1); + } + this.seenEvents = []; + }, +}; + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + + Services.prefs.setBoolPref("extensions.update.enabled", true); + Services.prefs.setBoolPref("extensions.update.autoUpdateDefault", true); + Services.prefs.setBoolPref("extensions.strictCompatibility", true); + Services.prefs.setBoolPref("extensions.checkUpdatesecurity", true); + + await promiseStartupManager(); + gManagerEventsListener.init(); + + // AddonManager.updateEnabled + gManagerEventsListener.expect(["onUpdateModeChanged"]); + AddonManager.updateEnabled = false; + gManagerEventsListener.checkExpected(); + Assert.ok(!AddonManager.updateEnabled); + Assert.ok(!Services.prefs.getBoolPref("extensions.update.enabled")); + + gManagerEventsListener.expect([]); + AddonManager.updateEnabled = false; + gManagerEventsListener.checkExpected(); + Assert.ok(!AddonManager.updateEnabled); + Assert.ok(!Services.prefs.getBoolPref("extensions.update.enabled")); + + gManagerEventsListener.expect(["onUpdateModeChanged"]); + AddonManager.updateEnabled = true; + gManagerEventsListener.checkExpected(); + Assert.ok(AddonManager.updateEnabled); + Assert.ok(Services.prefs.getBoolPref("extensions.update.enabled")); + + gManagerEventsListener.expect([]); + AddonManager.updateEnabled = true; + gManagerEventsListener.checkExpected(); + Assert.ok(AddonManager.updateEnabled); + Assert.ok(Services.prefs.getBoolPref("extensions.update.enabled")); + + // AddonManager.autoUpdateDefault + gManagerEventsListener.expect(["onUpdateModeChanged"]); + AddonManager.autoUpdateDefault = false; + gManagerEventsListener.checkExpected(); + Assert.ok(!AddonManager.autoUpdateDefault); + Assert.ok(!Services.prefs.getBoolPref("extensions.update.autoUpdateDefault")); + + gManagerEventsListener.expect([]); + AddonManager.autoUpdateDefault = false; + gManagerEventsListener.checkExpected(); + Assert.ok(!AddonManager.autoUpdateDefault); + Assert.ok(!Services.prefs.getBoolPref("extensions.update.autoUpdateDefault")); + + gManagerEventsListener.expect(["onUpdateModeChanged"]); + AddonManager.autoUpdateDefault = true; + gManagerEventsListener.checkExpected(); + Assert.ok(AddonManager.autoUpdateDefault); + Assert.ok(Services.prefs.getBoolPref("extensions.update.autoUpdateDefault")); + + gManagerEventsListener.expect([]); + AddonManager.autoUpdateDefault = true; + gManagerEventsListener.checkExpected(); + Assert.ok(AddonManager.autoUpdateDefault); + Assert.ok(Services.prefs.getBoolPref("extensions.update.autoUpdateDefault")); + + // AddonManager.strictCompatibility + gManagerEventsListener.expect(["onCompatibilityModeChanged"]); + AddonManager.strictCompatibility = false; + gManagerEventsListener.checkExpected(); + Assert.ok(!AddonManager.strictCompatibility); + Assert.ok(!Services.prefs.getBoolPref("extensions.strictCompatibility")); + + gManagerEventsListener.expect([]); + AddonManager.strictCompatibility = false; + gManagerEventsListener.checkExpected(); + Assert.ok(!AddonManager.strictCompatibility); + Assert.ok(!Services.prefs.getBoolPref("extensions.strictCompatibility")); + + gManagerEventsListener.expect(["onCompatibilityModeChanged"]); + AddonManager.strictCompatibility = true; + gManagerEventsListener.checkExpected(); + Assert.ok(AddonManager.strictCompatibility); + Assert.ok(Services.prefs.getBoolPref("extensions.strictCompatibility")); + + gManagerEventsListener.expect([]); + AddonManager.strictCompatibility = true; + gManagerEventsListener.checkExpected(); + Assert.ok(AddonManager.strictCompatibility); + Assert.ok(Services.prefs.getBoolPref("extensions.strictCompatibility")); + + // AddonManager.checkCompatibility + if (isNightlyChannel()) { + var version = "nightly"; + } else { + version = Services.appinfo.version.replace( + /^([^\.]+\.[0-9]+[a-z]*).*/gi, + "$1" + ); + } + const COMPATIBILITY_PREF = "extensions.checkCompatibility." + version; + + gManagerEventsListener.expect(["onCompatibilityModeChanged"]); + AddonManager.checkCompatibility = false; + gManagerEventsListener.checkExpected(); + Assert.ok(!AddonManager.checkCompatibility); + Assert.ok(!Services.prefs.getBoolPref(COMPATIBILITY_PREF)); + + gManagerEventsListener.expect([]); + AddonManager.checkCompatibility = false; + gManagerEventsListener.checkExpected(); + Assert.ok(!AddonManager.checkCompatibility); + Assert.ok(!Services.prefs.getBoolPref(COMPATIBILITY_PREF)); + + gManagerEventsListener.expect(["onCompatibilityModeChanged"]); + AddonManager.checkCompatibility = true; + gManagerEventsListener.checkExpected(); + Assert.ok(AddonManager.checkCompatibility); + Assert.ok(!Services.prefs.prefHasUserValue(COMPATIBILITY_PREF)); + + gManagerEventsListener.expect([]); + AddonManager.checkCompatibility = true; + gManagerEventsListener.checkExpected(); + Assert.ok(AddonManager.checkCompatibility); + Assert.ok(!Services.prefs.prefHasUserValue(COMPATIBILITY_PREF)); + + // AddonManager.checkUpdateSecurity + gManagerEventsListener.expect(["onCheckUpdateSecurityChanged"]); + AddonManager.checkUpdateSecurity = false; + gManagerEventsListener.checkExpected(); + Assert.ok(!AddonManager.checkUpdateSecurity); + if (AddonManager.checkUpdateSecurityDefault) { + Assert.ok(!Services.prefs.getBoolPref("extensions.checkUpdateSecurity")); + } else { + Assert.ok( + !Services.prefs.prefHasUserValue("extensions.checkUpdateSecurity") + ); + } + + gManagerEventsListener.expect([]); + AddonManager.checkUpdateSecurity = false; + gManagerEventsListener.checkExpected(); + Assert.ok(!AddonManager.checkUpdateSecurity); + if (AddonManager.checkUpdateSecurityDefault) { + Assert.ok(!Services.prefs.getBoolPref("extensions.checkUpdateSecurity")); + } else { + Assert.ok( + !Services.prefs.prefHasUserValue("extensions.checkUpdateSecurity") + ); + } + + gManagerEventsListener.expect(["onCheckUpdateSecurityChanged"]); + AddonManager.checkUpdateSecurity = true; + gManagerEventsListener.checkExpected(); + Assert.ok(AddonManager.checkUpdateSecurity); + if (!AddonManager.checkUpdateSecurityDefault) { + Assert.ok(Services.prefs.getBoolPref("extensions.checkUpdateSecurity")); + } else { + Assert.ok( + !Services.prefs.prefHasUserValue("extensions.checkUpdateSecurity") + ); + } + + gManagerEventsListener.expect([]); + AddonManager.checkUpdateSecurity = true; + gManagerEventsListener.checkExpected(); + Assert.ok(AddonManager.checkUpdateSecurity); + if (!AddonManager.checkUpdateSecurityDefault) { + Assert.ok(Services.prefs.getBoolPref("extensions.checkUpdateSecurity")); + } else { + Assert.ok( + !Services.prefs.prefHasUserValue("extensions.checkUpdateSecurity") + ); + } + + gManagerEventsListener.shutdown(); + + // After removing the listener, ensure we get no further events. + gManagerEventsListener.expect([]); + AddonManager.updateEnabled = false; + gManagerEventsListener.checkExpected(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_provider_markSafe.js b/toolkit/mozapps/extensions/test/xpcshell/test_provider_markSafe.js new file mode 100644 index 0000000000..e8062a2caf --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_provider_markSafe.js @@ -0,0 +1,43 @@ +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + +function mockAddonProvider(name) { + let mockProvider = { + markSafe: false, + apiAccessed: false, + + startup() { + if (this.markSafe) { + AddonManagerPrivate.markProviderSafe(this); + } + + AddonManager.isInstallEnabled("made-up-mimetype"); + }, + supportsMimetype(mimetype) { + this.apiAccessed = true; + return false; + }, + + get name() { + return name; + }, + }; + + return mockProvider; +} + +add_task(async function testMarkSafe() { + info("Starting with provider normally"); + let provider = mockAddonProvider("Mock1"); + AddonManagerPrivate.registerProvider(provider); + await promiseStartupManager(); + ok(!provider.apiAccessed, "Provider API should not have been accessed"); + AddonManagerPrivate.unregisterProvider(provider); + await promiseShutdownManager(); + + info("Starting with provider that marks itself safe"); + provider.apiAccessed = false; + provider.markSafe = true; + AddonManagerPrivate.registerProvider(provider); + await promiseStartupManager(); + ok(provider.apiAccessed, "Provider API should have been accessed"); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_provider_shutdown.js b/toolkit/mozapps/extensions/test/xpcshell/test_provider_shutdown.js new file mode 100644 index 0000000000..498b28a0c9 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_provider_shutdown.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Verify that we report shutdown status for Addon Manager providers +// and AddonRepository correctly. + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + +// Make a mock AddonRepository that just lets us hang shutdown. +// Needs two promises - one to let us know that AM has called shutdown, +// and one for us to let AM know that shutdown is done. +function mockAddonProvider(aName) { + let mockProvider = { + donePromise: null, + doneResolve: null, + doneReject: null, + shutdownPromise: null, + shutdownResolve: null, + + get name() { + return aName; + }, + + shutdown() { + this.shutdownResolve(); + return this.donePromise; + }, + }; + mockProvider.donePromise = new Promise((resolve, reject) => { + mockProvider.doneResolve = resolve; + mockProvider.doneReject = reject; + }); + mockProvider.shutdownPromise = new Promise((resolve, reject) => { + mockProvider.shutdownResolve = resolve; + }); + return mockProvider; +} + +// Helper to find a particular shutdown blocker's status in the JSON blob +function findInStatus(aStatus, aName) { + for (let { name, state } of aStatus.state) { + if (name == aName) { + return state; + } + } + return null; +} + +/* + * Make sure we report correctly when an add-on provider or AddonRepository block shutdown + */ +add_task(async function blockRepoShutdown() { + // the mock provider behaves enough like AddonRepository for the purpose of this test + let mockRepo = mockAddonProvider("Mock repo"); + AddonManagerPrivate.overrideAddonRepository(mockRepo); + + let mockProvider = mockAddonProvider("Mock provider"); + + await promiseStartupManager(); + AddonManagerPrivate.registerProvider(mockProvider); + + let { fetchState } = + MockAsyncShutdown.profileBeforeChange.blockers[0].options; + + // Start shutting the manager down + let managerDown = promiseShutdownManager(); + + // Wait for manager to call provider shutdown. + await mockProvider.shutdownPromise; + // check AsyncShutdown state + let status = fetchState(); + equal(findInStatus(status[1], "Mock provider"), "(none)"); + equal(status[2].name, "AddonRepository: async shutdown"); + equal(status[2].state, "pending"); + // let the provider finish + mockProvider.doneResolve(); + + // Wait for manager to call repo shutdown and start waiting for it + await mockRepo.shutdownPromise; + // Check the shutdown state + status = fetchState(); + equal(status[1].name, "AddonManager: Waiting for providers to shut down."); + equal(status[1].state, "Complete"); + equal(status[2].name, "AddonRepository: async shutdown"); + equal(status[2].state, "in progress"); + + // Now finish our shutdown, and wait for the manager to wrap up + mockRepo.doneResolve(); + await managerDown; + + // Check the shutdown state again + status = fetchState(); + equal(status[0].name, "AddonRepository: async shutdown"); + equal(status[0].state, "done"); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_shutdown.js b/toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_shutdown.js new file mode 100644 index 0000000000..720ccaf0c4 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_shutdown.js @@ -0,0 +1,65 @@ +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + +var shutdownOrder = []; + +function mockAddonProvider(name) { + let mockProvider = { + hasShutdown: false, + unsafeAccess: false, + + shutdownCallback: null, + + startup() {}, + shutdown() { + this.hasShutdown = true; + shutdownOrder.push(this.name); + if (this.shutdownCallback) { + return this.shutdownCallback(); + } + return undefined; + }, + getAddonByID(id, callback) { + if (this.hasShutdown) { + this.unsafeAccess = true; + } + callback(null); + }, + + get name() { + return name; + }, + }; + + return mockProvider; +} + +add_task(async function unsafeProviderShutdown() { + let firstProvider = mockAddonProvider("Mock1"); + AddonManagerPrivate.registerProvider(firstProvider); + let secondProvider = mockAddonProvider("Mock2"); + AddonManagerPrivate.registerProvider(secondProvider); + + await promiseStartupManager(); + + let shutdownPromise = null; + await new Promise(resolve => { + secondProvider.shutdownCallback = function () { + return AddonManager.getAddonByID("does-not-exist").then(() => { + resolve(); + }); + }; + + shutdownPromise = promiseShutdownManager(); + }); + await shutdownPromise; + + equal( + shutdownOrder.join(","), + ["Mock1", "Mock2"].join(","), + "Mock providers should have shutdown in expected order" + ); + ok( + !firstProvider.unsafeAccess, + "First registered mock provider should not have been accessed unsafely" + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_startup.js b/toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_startup.js new file mode 100644 index 0000000000..8e066973f2 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_startup.js @@ -0,0 +1,59 @@ +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + +var startupOrder = []; + +function mockAddonProvider(name) { + let mockProvider = { + hasStarted: false, + unsafeAccess: false, + + startupCallback: null, + + startup() { + this.hasStarted = true; + startupOrder.push(this.name); + if (this.startupCallback) { + this.startupCallback(); + } + }, + getAddonByID(id, callback) { + if (!this.hasStarted) { + this.unsafeAccess = true; + } + callback(null); + }, + + get name() { + return name; + }, + }; + + return mockProvider; +} + +add_task(async function unsafeProviderStartup() { + let secondProvider = null; + + await new Promise(resolve => { + let firstProvider = mockAddonProvider("Mock1"); + firstProvider.startupCallback = function () { + resolve(AddonManager.getAddonByID("does-not-exist")); + }; + AddonManagerPrivate.registerProvider(firstProvider); + + secondProvider = mockAddonProvider("Mock2"); + AddonManagerPrivate.registerProvider(secondProvider); + + promiseStartupManager(); + }); + + equal( + startupOrder.join(","), + ["Mock1", "Mock2"].join(","), + "Mock providers should have hasStarted in expected order" + ); + ok( + !secondProvider.unsafeAccess, + "Second registered mock provider should not have been accessed unsafely" + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_proxies.js b/toolkit/mozapps/extensions/test/xpcshell/test_proxies.js new file mode 100644 index 0000000000..2f40147f83 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_proxies.js @@ -0,0 +1,235 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests the semantics of extension proxy files and symlinks + +var ADDONS = [ + { + id: "proxy1@tests.mozilla.org", + dirId: "proxy1@tests.mozilla.com", + type: "proxy", + }, + { + id: "proxy2@tests.mozilla.org", + type: "proxy", + }, + { + id: "symlink1@tests.mozilla.org", + dirId: "symlink1@tests.mozilla.com", + type: "symlink", + }, + { + id: "symlink2@tests.mozilla.org", + type: "symlink", + }, +]; + +const gHaveSymlinks = AppConstants.platform != "win"; + +function createSymlink(aSource, aDest) { + if (aSource instanceof Ci.nsIFile) { + aSource = aSource.path; + } + if (aDest instanceof Ci.nsIFile) { + aDest = aDest.path; + } + + const ln = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + ln.initWithPath("/bin/ln"); + + const lnProcess = Cc["@mozilla.org/process/util;1"].createInstance( + Ci.nsIProcess + ); + lnProcess.init(ln); + + const args = ["-s", aSource, aDest]; + lnProcess.run(true, args, args.length); + Assert.equal(lnProcess.exitValue, 0); +} + +async function promiseWriteFile(aFile, aData) { + if (!(await IOUtils.exists(aFile.parent.path))) { + await IOUtils.makeDirectory(aFile.parent.path); + } + + return IOUtils.writeUTF8(aFile.path, aData); +} + +function checkAddonsExist() { + for (let addon of ADDONS) { + let file = addon.directory.clone(); + file.append("manifest.json"); + Assert.ok(file.exists()); + } +} + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +function run_test() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2"); + + // Unpacked extensions are never signed, so this can only run with + // signature checks disabled. + Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, false); + + add_task(run_proxy_tests); + + if (gHaveSymlinks) { + add_task(run_symlink_tests); + } + + run_next_test(); +} + +async function run_proxy_tests() { + if (!gHaveSymlinks) { + ADDONS = ADDONS.filter(a => a.type != "symlink"); + } + + for (let addon of ADDONS) { + addon.directory = gTmpD.clone(); + addon.directory.append(addon.id); + + addon.proxyFile = profileDir.clone(); + addon.proxyFile.append(addon.dirId || addon.id); + + let files = ExtensionTestCommon.generateFiles({ + manifest: { + name: addon.id, + browser_specific_settings: { gecko: { id: addon.id } }, + }, + }); + let path = PathUtils.join(gTmpD.path, addon.id); + await AddonTestUtils.promiseWriteFilesToDir(path, files); + + if (addon.type == "proxy") { + await promiseWriteFile(addon.proxyFile, addon.directory.path); + } else if (addon.type == "symlink") { + await createSymlink(addon.directory, addon.proxyFile); + } + } + + await promiseStartupManager(); + + // Check that all add-ons original sources still exist after invalid + // add-ons have been removed at startup. + checkAddonsExist(); + + let addons = await AddonManager.getAddonsByIDs(ADDONS.map(addon => addon.id)); + try { + for (let [i, addon] of addons.entries()) { + // Ensure that valid proxied add-ons were installed properly on + // platforms that support the installation method. + print( + ADDONS[i].id, + ADDONS[i].dirId, + ADDONS[i].dirId != null, + ADDONS[i].type == "symlink" + ); + Assert.equal(addon == null, ADDONS[i].dirId != null); + + if (addon != null) { + let fixURL = url => { + if (AppConstants.platform == "macosx") { + return url.replace(RegExp(`^file:///private/`), "file:///"); + } + return url; + }; + + // Check that proxied add-ons do not have upgrade permissions. + Assert.equal(addon.permissions & AddonManager.PERM_CAN_UPGRADE, 0); + + // Check that getResourceURI points to the right place. + Assert.equal( + Services.io.newFileURI(ADDONS[i].directory).spec, + fixURL(addon.getResourceURI().spec), + `Base resource URL resolves as expected` + ); + + let file = ADDONS[i].directory.clone(); + file.append("manifest.json"); + + Assert.equal( + Services.io.newFileURI(file).spec, + fixURL(addon.getResourceURI("manifest.json").spec), + `Resource URLs resolve as expected` + ); + + await addon.uninstall(); + } + } + + // Check that original sources still exist after explicit uninstall. + await promiseRestartManager(); + checkAddonsExist(); + + await promiseShutdownManager(); + + // Check that all of the proxy files have been removed and remove + // the original targets. + for (let addon of ADDONS) { + equal( + addon.proxyFile.exists(), + addon.dirId != null, + `Proxy file ${addon.proxyFile.path} should exist?` + ); + addon.directory.remove(true); + try { + addon.proxyFile.remove(false); + } catch (e) {} + } + } catch (e) { + do_throw(e); + } +} + +// Check that symlinks are not followed out of a directory tree +// when deleting an add-on. +async function run_symlink_tests() { + const ID = "unpacked@test.mozilla.org"; + + let tempDirectory = gTmpD.clone(); + tempDirectory.append(ID); + + let tempFile = tempDirectory.clone(); + tempFile.append("test.txt"); + tempFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644); + + let addonDirectory = profileDir.clone(); + addonDirectory.append(ID); + + let files = ExtensionTestCommon.generateFiles({ + manifest: { browser_specific_settings: { gecko: { id: ID } } }, + }); + await AddonTestUtils.promiseWriteFilesToDir(addonDirectory.path, files); + + let symlink = addonDirectory.clone(); + symlink.append(tempDirectory.leafName); + await createSymlink(tempDirectory, symlink); + + // Make sure that the symlink was created properly. + let file = symlink.clone(); + file.append(tempFile.leafName); + file.normalize(); + Assert.equal(file.path.replace(/^\/private\//, "/"), tempFile.path); + + await promiseStartupManager(); + + let addon = await AddonManager.getAddonByID(ID); + Assert.notEqual(addon, null); + + await addon.uninstall(); + + await promiseRestartManager(); + await promiseShutdownManager(); + + // Check that the install directory is gone. + Assert.ok(!addonDirectory.exists()); + + // Check that the temp file is not gone. + Assert.ok(tempFile.exists()); + + tempDirectory.remove(true); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_recommendations.js b/toolkit/mozapps/extensions/test/xpcshell/test_recommendations.js new file mode 100644 index 0000000000..2ea7c0f77f --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_recommendations.js @@ -0,0 +1,707 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { XPIExports } = ChromeUtils.importESModule( + "resource://gre/modules/addons/XPIExports.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs", + Management: "resource://gre/modules/Extension.sys.mjs", +}); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.usePrivilegedSignatures = false; + +const testStartTime = Date.now(); +const not_before = new Date(testStartTime - 3600000).toISOString(); +const not_after = new Date(testStartTime + 3600000).toISOString(); +const RECOMMENDATION_FILE_NAME = "mozilla-recommendation.json"; + +const server = AddonTestUtils.createHttpServer(); +const SERVER_BASE_URL = `http://localhost:${server.identity.primaryPort}`; +// Allow the test extensions to be updated from an insecure update url. +Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false); +Services.prefs.setCharPref( + "extensions.update.background.url", + `${SERVER_BASE_URL}/upgrade.json` +); + +function createFileWithRecommendations(id, recommendation, version = "1.0.0") { + let files = {}; + if (recommendation) { + files[RECOMMENDATION_FILE_NAME] = recommendation; + } + return AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version, + browser_specific_settings: { gecko: { id } }, + }, + files, + }); +} + +async function installAddonWithRecommendations(id, recommendation) { + let xpi = createFileWithRecommendations(id, recommendation); + let install = await AddonTestUtils.promiseInstallFile(xpi); + return install.addon; +} + +function checkRecommended(addon, recommended = true) { + equal( + addon.isRecommended, + recommended, + "The add-on isRecommended state is correct" + ); + equal( + addon.recommendationStates.includes("recommended"), + recommended, + "The add-on recommendationStates is correct" + ); +} + +function waitForPendingExtension(extId) { + return new Promise(resolve => { + Management.on("startup", function startupListener() { + const pendingExtensionsMap = + Services.ppmm.sharedData.get("extensions/pending"); + if (pendingExtensionsMap.has(extId)) { + Management.off("startup", startupListener); + resolve(pendingExtensionsMap.get(extId)); + } + }); + }); +} + +async function assertPendingExtensionIgnoreQuarantined({ + addonId, + expectedIgnoreQuarantined, +}) { + info( + `Reload ${addonId} and verify ignoreQuarantine in extensions/pending sharedData` + ); + const promisePendingExtension = waitForPendingExtension(addonId); + const addon = await AddonManager.getAddonByID(addonId); + await addon.disable(); + await addon.enable(); + Assert.deepEqual( + (await promisePendingExtension).ignoreQuarantine, + expectedIgnoreQuarantined, + `Expect ignoreQuarantine to be true in pending/extensions details for ${addon.id}` + ); +} + +function assertQuarantinedFromURI({ domain, expected }) { + const { processType, PROCESS_TYPE_DEFAULT } = Services.appinfo; + const processTypeStr = + processType === PROCESS_TYPE_DEFAULT ? "Main Process" : "Child Process"; + const testURI = Services.io.newURI(`https://${domain}/`); + for (const [addonId, expectedQuarantinedFromURI] of Object.entries( + expected + )) { + Assert.equal( + WebExtensionPolicy.getByID(addonId).quarantinedFromURI(testURI), + expectedQuarantinedFromURI, + `Expect ${addonId} to ${ + expectedQuarantinedFromURI ? "not be" : "be" + } quarantined from ${domain} in ${processTypeStr}` + ); + } +} + +async function assertQuarantinedFromURIInChildProcessAsync({ + domain, + expected, +}) { + // Doesn't matter what content url we us here, as long as we are + // using a content url to be able to run the assertions from a + // child process. + const testUrl = SERVER_BASE_URL; + const page = await ExtensionTestUtils.loadContentPage(testUrl); + // TODO(rpl): look into Bug 1648545 changes and determine what + // would need to change to use page.spawn instead. + await page.legacySpawn({ domain, expected }, assertQuarantinedFromURI); + await page.close(); +} + +function getUpdatesJSONFor(id, version) { + return { + updates: [ + { + version, + update_link: `${SERVER_BASE_URL}/addons/${id}.xpi`, + }, + ], + }; +} + +function registerUpdateXPIFile({ id, version, recommendationStates }) { + const recommendation = { + addon_id: id, + states: recommendationStates, + validity: { not_before, not_after }, + }; + let xpi = createFileWithRecommendations(id, recommendation, version); + server.registerFile(`/addons/${id}.xpi`, xpi); +} + +function waitForBootstrapUpdateMethod(addonId, newVersion) { + return new Promise(resolve => { + function listener(_evt, { method, params }) { + if ( + method === "update" && + params.id === addonId && + params.newVersion === newVersion + ) { + AddonTestUtils.off("bootstrap-method", listener); + info(`Update bootstrap method called for ${addonId} ${newVersion}`); + resolve({ addonId, method, params }); + } + } + AddonTestUtils.on("bootstrap-method", listener); + }); +} + +function assertUpdateBootstrapCall(detailsBootstrapUpdates, expected) { + const actualPerAddonId = detailsBootstrapUpdates + .map(({ addonId, params }) => { + return [addonId, params.recommendationState?.states]; + }) + .reduce((acc, [addonId, states]) => { + acc[addonId] = states; + return acc; + }, {}); + Assert.deepEqual( + actualPerAddonId, + expected, + `Got the expected recommendation states in the update bootstrap calls` + ); +} + +add_setup(async () => { + await ExtensionTestUtils.startAddonManager(); +}); + +add_task(async function text_no_file() { + const id = "no-recommendations-file@test.web.extension"; + let addon = await installAddonWithRecommendations(id, null); + + checkRecommended(addon, false); + + await addon.uninstall(); +}); + +add_task(async function text_malformed_file() { + const id = "no-recommendations-file@test.web.extension"; + let addon = await installAddonWithRecommendations(id, "This is not JSON"); + + checkRecommended(addon, false); + + await addon.uninstall(); +}); + +add_task(async function test_valid_recommendation_file() { + const id = "recommended@test.web.extension"; + let addon = await installAddonWithRecommendations(id, { + addon_id: id, + states: ["recommended"], + validity: { not_before, not_after }, + }); + + checkRecommended(addon); + + await addon.uninstall(); +}); + +add_task(async function test_multiple_valid_recommendation_file() { + const id = "recommended@test.web.extension"; + let addon = await installAddonWithRecommendations(id, { + addon_id: id, + states: ["recommended", "something"], + validity: { not_before, not_after }, + }); + + checkRecommended(addon); + ok( + addon.recommendationStates.includes("something"), + "The add-on recommendationStates contains something" + ); + + await addon.uninstall(); +}); + +add_task(async function test_unsigned() { + // Don't override the certificate, so that the test add-on is unsigned. + AddonTestUtils.useRealCertChecks = true; + // Allow unsigned add-on to be installed. + Services.prefs.setBoolPref("xpinstall.signatures.required", false); + + const id = "unsigned@test.web.extension"; + let addon = await installAddonWithRecommendations(id, { + addon_id: id, + states: ["recommended"], + validity: { not_before, not_after }, + }); + + checkRecommended(addon, false); + + await addon.uninstall(); + AddonTestUtils.useRealCertChecks = false; + Services.prefs.setBoolPref("xpinstall.signatures.required", true); +}); + +add_task(async function test_temporary() { + const id = "temporary@test.web.extension"; + let xpi = createFileWithRecommendations(id, { + addon_id: id, + states: ["recommended"], + validity: { not_before, not_after }, + }); + let addon = await XPIExports.XPIInstall.installTemporaryAddon(xpi); + + checkRecommended(addon, false); + + await addon.uninstall(); +}); + +// Tests that unpacked temporary add-ons are not recommended. +add_task(async function test_temporary_directory() { + const id = "temporary-dir@test.web.extension"; + let files = ExtensionTestCommon.generateFiles({ + manifest: { + browser_specific_settings: { gecko: { id } }, + }, + files: { + [RECOMMENDATION_FILE_NAME]: { + addon_id: id, + states: ["recommended"], + validity: { not_before, not_after }, + }, + }, + }); + let extDir = await AddonTestUtils.promiseWriteFilesToExtension( + gTmpD.path, + id, + files, + true + ); + + let addon = await XPIExports.XPIInstall.installTemporaryAddon(extDir); + + checkRecommended(addon, false); + + await addon.uninstall(); + extDir.remove(true); +}); + +add_task(async function test_builtin() { + const id = "builtin@test.web.extension"; + let extension = await installBuiltinExtension({ + manifest: { + browser_specific_settings: { gecko: { id } }, + }, + background: `browser.test.sendMessage("started");`, + files: { + [RECOMMENDATION_FILE_NAME]: { + addon_id: id, + states: ["recommended"], + validity: { not_before, not_after }, + }, + }, + }); + await extension.awaitMessage("started"); + + checkRecommended(extension.addon, false); + + await extension.unload(); +}); + +add_task(async function test_theme() { + const id = "theme@test.web.extension"; + let xpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { gecko: { id } }, + theme: {}, + }, + files: { + [RECOMMENDATION_FILE_NAME]: { + addon_id: id, + states: ["recommended"], + validity: { not_before, not_after }, + }, + }, + }); + let { addon } = await AddonTestUtils.promiseInstallFile(xpi); + + checkRecommended(addon, false); + + await addon.uninstall(); +}); + +add_task(async function test_not_recommended() { + const id = "not-recommended@test.web.extension"; + let addon = await installAddonWithRecommendations(id, { + addon_id: id, + states: ["something"], + validity: { not_before, not_after }, + }); + + checkRecommended(addon, false); + ok( + addon.recommendationStates.includes("something"), + "The add-on recommendationStates contains something" + ); + + await addon.uninstall(); +}); + +add_task(async function test_id_missing() { + const id = "no-id@test.web.extension"; + let addon = await installAddonWithRecommendations(id, { + states: ["recommended"], + validity: { not_before, not_after }, + }); + + checkRecommended(addon, false); + + await addon.uninstall(); +}); + +add_task(async function test_expired() { + const id = "expired@test.web.extension"; + let addon = await installAddonWithRecommendations(id, { + addon_id: id, + states: ["recommended", "something"], + validity: { not_before, not_after: not_before }, + }); + + checkRecommended(addon, false); + ok( + !addon.recommendationStates.length, + "The add-on recommendationStates does not contain anything" + ); + + await addon.uninstall(); +}); + +add_task(async function test_not_valid_yet() { + const id = "expired@test.web.extension"; + let addon = await installAddonWithRecommendations(id, { + addon_id: id, + states: ["recommended"], + validity: { not_before: not_after, not_after }, + }); + + checkRecommended(addon, false); + + await addon.uninstall(); +}); + +add_task(async function test_states_missing() { + const id = "states-missing@test.web.extension"; + let addon = await installAddonWithRecommendations(id, { + addon_id: id, + validity: { not_before, not_after }, + }); + + checkRecommended(addon, false); + + await addon.uninstall(); +}); + +add_task(async function test_validity_missing() { + const id = "validity-missing@test.web.extension"; + let addon = await installAddonWithRecommendations(id, { + addon_id: id, + states: ["recommended"], + }); + + checkRecommended(addon, false); + + await addon.uninstall(); +}); + +add_task(async function test_not_before_missing() { + const id = "not-before-missing@test.web.extension"; + let addon = await installAddonWithRecommendations(id, { + addon_id: id, + states: ["recommended"], + validity: { not_after }, + }); + + checkRecommended(addon, false); + + await addon.uninstall(); +}); + +add_task(async function test_bad_states() { + const id = "bad-states@test.web.extension"; + let addon = await installAddonWithRecommendations(id, { + addon_id: id, + states: { recommended: true }, + validity: { not_before, not_after }, + }); + + checkRecommended(addon, false); + + await addon.uninstall(); +}); + +add_task(async function test_recommendation_persist_restart() { + const id = "persisted-recommendation@test.web.extension"; + let addon = await installAddonWithRecommendations(id, { + addon_id: id, + states: ["recommended"], + validity: { not_before, not_after }, + }); + + checkRecommended(addon); + + await AddonTestUtils.promiseRestartManager(); + + addon = await AddonManager.getAddonByID(id); + + checkRecommended(addon); + + await addon.uninstall(); +}); + +add_task(async function test_isLineExtension_internal_svg_permission() { + async function assertLineExtensionStateAndPermission( + addonId, + expectLineExtension, + isRestart + ) { + const { extension } = WebExtensionPolicy.getByID(addonId); + + const msgShould = expectLineExtension ? "should" : "should not"; + + equal( + extension.hasPermission("internal:svgContextPropertiesAllowed"), + expectLineExtension, + `"${addonId}" ${msgShould} have permission internal:svgContextPropertiesAllowed` + ); + if (isRestart) { + const { permissions } = await ExtensionPermissions.get(addonId); + Assert.deepEqual( + permissions, + expectLineExtension ? ["internal:svgContextPropertiesAllowed"] : [], + `ExtensionPermission.get("${addonId}") result ${msgShould} include internal:svgContextPropertiesAllowed permission` + ); + } + } + + const idLineExt = "line-extension@test.web.extension"; + await installAddonWithRecommendations(idLineExt, { + addon_id: idLineExt, + states: ["line"], + validity: { not_before, not_after }, + }); + + info(`Test line extension ${idLineExt}`); + await assertLineExtensionStateAndPermission(idLineExt, true, false); + await AddonTestUtils.promiseRestartManager(); + info(`Test ${idLineExt} again after AOM restart`); + await assertLineExtensionStateAndPermission(idLineExt, true, true); + let addon = await AddonManager.getAddonByID(idLineExt); + await addon.uninstall(); + + const idNonLineExt = "non-line-extension@test.web.extension"; + await installAddonWithRecommendations(idNonLineExt, { + addon_id: idNonLineExt, + states: ["recommended"], + validity: { not_before, not_after }, + }); + + info(`Test non line extension: ${idNonLineExt}`); + await assertLineExtensionStateAndPermission(idNonLineExt, false, false); + await AddonTestUtils.promiseRestartManager(); + info(`Test ${idNonLineExt} again after AOM restart`); + await assertLineExtensionStateAndPermission(idNonLineExt, false, true); + addon = await AddonManager.getAddonByID(idNonLineExt); + await addon.uninstall(); +}); + +add_task( + { + pref_set: [ + ["extensions.quarantinedDomains.enabled", true], + ["extensions.quarantinedDomains.list", "quarantined.example.org"], + ], + }, + async function test_recommended_exempt_from_quarantined() { + const invalidRecommendedId = "invalid-recommended@test.web.extension"; + const validRecommendedId = "recommended@test.web.extension"; + const validAndroidRecommendedId = "recommended-android@test.web.extension"; + const lineExtensionId = "line@test.web.extension"; + const validMultiRecommendedId = "recommended-multi@test.web.extension"; + // NOTE: confirm that any future recommendation state that was considered + // valid and signed by AMO is also going to be exempt, which does also include + // recommendation states that we are not using anymore but are still technically + // supported by autograph (e.g. verified), see: + // https://github.com/mozilla-services/autograph/blob/8a34847a/autograph.yaml#L1456-L1460 + const validFutureRecStateId = "fake-future-valid-state@test.web.extension"; + + const recommendationStatesPerId = { + [invalidRecommendedId]: null, + [validRecommendedId]: ["recommended"], + [validAndroidRecommendedId]: ["recommended-android"], + [lineExtensionId]: ["line"], + [validFutureRecStateId]: ["fake-future-valid-state"], + [validMultiRecommendedId]: ["recommended", "recommended-android"], + }; + + for (const [extId, expectedRecStates] of Object.entries( + recommendationStatesPerId + )) { + const recommendationData = expectedRecStates + ? { + addon_id: extId, + states: expectedRecStates, + validity: { not_before, not_after }, + } + : null; + await installAddonWithRecommendations(extId, recommendationData); + // Check that the expected recommendation states are reflected by the + // value returned by the AddonWrapper.recommendationStates getter. + const addon = await AddonManager.getAddonByID(extId); + Assert.deepEqual( + addon.recommendationStates, + expectedRecStates ?? [], + `Addon ${extId} has the expected recommendation states` + ); + } + + assertQuarantinedFromURI({ + domain: "quarantined.example.org", + expected: { + [invalidRecommendedId]: true, + [validRecommendedId]: false, + [validAndroidRecommendedId]: false, + [lineExtensionId]: false, + [validFutureRecStateId]: false, + [validMultiRecommendedId]: false, + }, + }); + + await assertQuarantinedFromURIInChildProcessAsync({ + domain: "quarantined.example.org", + expected: { + [invalidRecommendedId]: true, + [validRecommendedId]: false, + [validAndroidRecommendedId]: false, + [lineExtensionId]: false, + [validFutureRecStateId]: false, + [validMultiRecommendedId]: false, + }, + }); + + // NOTE: we only cover the 3 basic cases in the rest of this test case + // (we have verified that ignoreQuarantine is being set to the expected + // value and so the other cases shouldn't matter for the behaviors being + // explicitly covered by the remaining part of this test task). + + // Make sure the ignoreQuarantine property is also propagated in the child + // processes while the extensions may still be not fully initialized (and + // so listed in the `extensions/pending` sharedData entry). + await assertPendingExtensionIgnoreQuarantined({ + addonId: validRecommendedId, + expectedIgnoreQuarantined: true, + }); + await assertPendingExtensionIgnoreQuarantined({ + addonId: lineExtensionId, + expectedIgnoreQuarantined: true, + }); + await assertPendingExtensionIgnoreQuarantined({ + addonId: invalidRecommendedId, + expectedIgnoreQuarantined: false, + }); + + info("Verify ignoreQuarantine again after application restart"); + + await AddonTestUtils.promiseRestartManager(); + assertQuarantinedFromURI({ + domain: "quarantined.example.org", + expected: { + [invalidRecommendedId]: true, + [validRecommendedId]: false, + [lineExtensionId]: false, + }, + }); + + info("Verify ignoreQuarantine again after addon updates"); + + AddonTestUtils.registerJSON(server, "/upgrade.json", { + addons: { + [invalidRecommendedId]: getUpdatesJSONFor( + invalidRecommendedId, + "2.0.0" + ), + [validRecommendedId]: getUpdatesJSONFor(validRecommendedId, "2.0.0"), + [lineExtensionId]: getUpdatesJSONFor(lineExtensionId, "2.0.0"), + }, + }); + registerUpdateXPIFile({ + id: invalidRecommendedId, + version: "2.0.0", + recommendationStates: recommendationStatesPerId[invalidRecommendedId], + }); + registerUpdateXPIFile({ + id: validRecommendedId, + version: "2.0.0", + recommendationStates: recommendationStatesPerId[validRecommendedId], + }); + registerUpdateXPIFile({ + id: lineExtensionId, + version: "2.0.0", + recommendationStates: recommendationStatesPerId[lineExtensionId], + }); + + const promiseUpdatesInstalled = Promise.all([ + waitForBootstrapUpdateMethod(invalidRecommendedId, "2.0.0"), + waitForBootstrapUpdateMethod(validRecommendedId, "2.0.0"), + waitForBootstrapUpdateMethod(lineExtensionId, "2.0.0"), + ]); + + const promiseBackgroundUpdatesFound = TestUtils.topicObserved( + "addons-background-updates-found" + ); + let [ + extensionInvalidRecommended, + extensionValidRecommended, + extensionLine, + ] = [ + ExtensionTestUtils.expectExtension(invalidRecommendedId), + ExtensionTestUtils.expectExtension(validRecommendedId), + ExtensionTestUtils.expectExtension(lineExtensionId), + ]; + + await AddonManagerPrivate.backgroundUpdateCheck(); + await promiseBackgroundUpdatesFound; + + assertUpdateBootstrapCall(await promiseUpdatesInstalled, { + [invalidRecommendedId]: null, + [validRecommendedId]: ["recommended"], + [lineExtensionId]: ["line"], + }); + + // Wait the test extension to be fully started (prevents logspam + // due to the AOM trying to uninstall them while being started). + await Promise.all([ + extensionInvalidRecommended.awaitStartup(), + extensionValidRecommended.awaitStartup(), + extensionLine.awaitStartup(), + ]); + + // Uninstall all test extensions. + await Promise.all( + Object.keys(recommendationStatesPerId).map(async addonId => { + const addon = await AddonManager.getAddonByID(addonId); + await addon.uninstall(); + }) + ); + } +); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_registerchrome.js b/toolkit/mozapps/extensions/test/xpcshell/test_registerchrome.js new file mode 100644 index 0000000000..5d45a1a273 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_registerchrome.js @@ -0,0 +1,88 @@ +"use strict"; + +function getFileURI(path) { + let file = do_get_file("."); + file.append(path); + return Services.io.newFileURI(file); +} + +add_task(async function () { + const registry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService( + Ci.nsIChromeRegistry + ); + + let file1 = getFileURI("file1"); + let file2 = getFileURI("file2"); + + let uri1 = getFileURI("chrome.manifest"); + let uri2 = getFileURI("manifest.json"); + + let overrideURL = Services.io.newURI("chrome://global/content/foo"); + let contentURL = Services.io.newURI("chrome://test/content/foo"); + let localeURL = Services.io.newURI("chrome://global/locale/foo"); + + let origOverrideURL = registry.convertChromeURL(overrideURL); + let origLocaleURL = registry.convertChromeURL(localeURL); + + let entry1 = aomStartup.registerChrome(uri1, [ + ["override", "chrome://global/content/foo", file1.spec], + ["content", "test", file2.spec + "/"], + ["locale", "global", "en-US", file2.spec + "/"], + ]); + + let entry2 = aomStartup.registerChrome(uri2, [ + ["override", "chrome://global/content/foo", file2.spec], + ["content", "test", file1.spec + "/"], + ["locale", "global", "en-US", file1.spec + "/"], + ]); + + // Initially, the second entry should override the first. + equal(registry.convertChromeURL(overrideURL).spec, file2.spec); + let file = file1.spec + "/foo"; + equal(registry.convertChromeURL(contentURL).spec, file); + equal(registry.convertChromeURL(localeURL).spec, file); + + // After destroying the second entry, the first entry should now take + // precedence. + entry2.destruct(); + equal(registry.convertChromeURL(overrideURL).spec, file1.spec); + file = file2.spec + "/foo"; + equal(registry.convertChromeURL(contentURL).spec, file); + equal(registry.convertChromeURL(localeURL).spec, file); + + // After dropping the reference to the first entry and allowing it to + // be GCed, we should be back to the original entries. + entry1 = null; // eslint-disable-line no-unused-vars + Cu.forceGC(); + Cu.forceCC(); + equal(registry.convertChromeURL(overrideURL).spec, origOverrideURL.spec); + equal(registry.convertChromeURL(localeURL).spec, origLocaleURL.spec); + Assert.throws( + () => registry.convertChromeURL(contentURL), + e => e.result == Cr.NS_ERROR_FILE_NOT_FOUND, + "chrome://test/ should no longer be registered" + ); +}); + +add_task(async function () { + const INVALID_VALUES = [ + {}, + "foo", + ["foo"], + [{}], + [[]], + [["locale", "global"]], + [["locale", "global", "en", "foo", "foo"]], + [["override", "en"]], + [["override", "en", "US", "OR"]], + ]; + + let uri = getFileURI("chrome.manifest"); + for (let arg of INVALID_VALUES) { + Assert.throws( + () => aomStartup.registerChrome(uri, arg), + e => e.result == Cr.NS_ERROR_INVALID_ARG, + `Arg ${uneval(arg)} should throw` + ); + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_registry.js b/toolkit/mozapps/extensions/test/xpcshell/test_registry.js new file mode 100644 index 0000000000..22909e6362 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_registry.js @@ -0,0 +1,160 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that extensions installed through the registry work as expected +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + +// Enable loading extensions from the user and system scopes +Services.prefs.setIntPref( + "extensions.enabledScopes", + AddonManager.SCOPE_PROFILE + + AddonManager.SCOPE_USER + + AddonManager.SCOPE_SYSTEM +); + +Services.prefs.setIntPref("extensions.sideloadScopes", AddonManager.SCOPE_ALL); + +const ID1 = "addon1@tests.mozilla.org"; +const ID2 = "addon2@tests.mozilla.org"; +let xpi1, xpi2; + +let registry; + +add_task(async function setup() { + xpi1 = await createTempWebExtensionFile({ + manifest: { browser_specific_settings: { gecko: { id: ID1 } } }, + }); + + xpi2 = await createTempWebExtensionFile({ + manifest: { browser_specific_settings: { gecko: { id: ID2 } } }, + }); + + registry = new MockRegistry(); + registerCleanupFunction(() => { + registry.shutdown(); + }); +}); + +// Tests whether basic registry install works +add_task(async function test_1() { + registry.setValue( + Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, + "SOFTWARE\\Mozilla\\XPCShell\\Extensions", + ID1, + xpi1.path + ); + registry.setValue( + Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + "SOFTWARE\\Mozilla\\XPCShell\\Extensions", + ID2, + xpi2.path + ); + + await promiseStartupManager(); + + let [a1, a2] = await AddonManager.getAddonsByIDs([ID1, ID2]); + notEqual(a1, null); + ok(a1.isActive); + ok(!hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL)); + equal(a1.scope, AddonManager.SCOPE_SYSTEM); + + notEqual(a2, null); + ok(a2.isActive); + ok(!hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL)); + equal(a2.scope, AddonManager.SCOPE_USER); +}); + +// Tests whether uninstalling from the registry works +add_task(async function test_2() { + registry.setValue( + Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, + "SOFTWARE\\Mozilla\\XPCShell\\Extensions", + ID1, + null + ); + registry.setValue( + Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + "SOFTWARE\\Mozilla\\XPCShell\\Extensions", + ID2, + null + ); + + await promiseRestartManager(); + + let [a1, a2] = await AddonManager.getAddonsByIDs([ID1, ID2]); + equal(a1, null); + equal(a2, null); +}); + +// Checks that the ID in the registry must match that in the install manifest +add_task(async function test_3() { + registry.setValue( + Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, + "SOFTWARE\\Mozilla\\XPCShell\\Extensions", + ID1, + xpi2.path + ); + registry.setValue( + Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + "SOFTWARE\\Mozilla\\XPCShell\\Extensions", + ID2, + xpi1.path + ); + + await promiseRestartManager(); + + let [a1, a2] = await AddonManager.getAddonsByIDs([ID1, ID2]); + equal(a1, null); + equal(a2, null); +}); + +// Tests whether an extension's ID can change without its directory changing +add_task(async function test_4() { + // Restarting with bad items in the registry should not force an EM restart + await promiseRestartManager(); + + registry.setValue( + Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, + "SOFTWARE\\Mozilla\\XPCShell\\Extensions", + ID1, + null + ); + registry.setValue( + Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + "SOFTWARE\\Mozilla\\XPCShell\\Extensions", + ID2, + null + ); + + await promiseRestartManager(); + + registry.setValue( + Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, + "SOFTWARE\\Mozilla\\XPCShell\\Extensions", + ID1, + xpi1.path + ); + + await promiseShutdownManager(); + + registry.setValue( + Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, + "SOFTWARE\\Mozilla\\XPCShell\\Extensions", + ID1, + null + ); + registry.setValue( + Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + "SOFTWARE\\Mozilla\\XPCShell\\Extensions", + ID2, + xpi1.path + ); + xpi2.copyTo(xpi1.parent, xpi1.leafName); + + await promiseStartupManager(); + + let [a1, a2] = await AddonManager.getAddonsByIDs([ID1, ID2]); + equal(a1, null); + notEqual(a2, null); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_reinstall_disabled_addon.js b/toolkit/mozapps/extensions/test/xpcshell/test_reinstall_disabled_addon.js new file mode 100644 index 0000000000..f231397072 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_reinstall_disabled_addon.js @@ -0,0 +1,213 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const ID = "test_addon@tests.mozilla.org"; + +const ADDONS = { + test_install1_1: { + name: "Test 1 Addon", + description: "Test 1 addon description", + manifest_version: 2, + version: "1.0", + browser_specific_settings: { + gecko: { + id: ID, + }, + }, + }, + test_install1_2: { + name: "Test 1 Addon", + description: "Test 1 addon description", + manifest_version: 2, + version: "2.0", + browser_specific_settings: { + gecko: { + id: ID, + }, + }, + }, +}; + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + await promiseStartupManager(); +}); + +// User intentionally reinstalls existing disabled addon of the same version. +// No onInstalling nor onInstalled are fired. +add_task(async function reinstallExistingDisabledAddonSameVersion() { + await expectEvents( + { + ignorePlugins: true, + addonEvents: { + [ID]: [{ event: "onInstalling" }, { event: "onInstalled" }], + }, + installEvents: [ + { event: "onNewInstall" }, + { event: "onInstallStarted" }, + { event: "onInstallEnded" }, + ], + }, + async () => { + const xpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: ADDONS.test_install1_1, + }); + let install = await AddonManager.getInstallForFile(xpi); + await install.install(); + } + ); + + let addon = await promiseAddonByID(ID); + notEqual(addon, null); + equal(addon.pendingOperations, AddonManager.PENDING_NONE); + ok(addon.isActive); + ok(!addon.userDisabled); + + await expectEvents( + { + ignorePlugins: true, + addonEvents: { + [ID]: [{ event: "onDisabling" }, { event: "onDisabled" }], + }, + }, + () => addon.disable() + ); + + addon = await promiseAddonByID(ID); + notEqual(addon, null); + equal(addon.pendingOperations, AddonManager.PENDING_NONE); + ok(!addon.isActive); + ok(addon.userDisabled); + + await expectEvents( + { + ignorePlugins: true, + addonEvents: { + [ID]: [{ event: "onEnabling" }, { event: "onEnabled" }], + }, + installEvents: [ + { event: "onNewInstall" }, + { event: "onInstallStarted" }, + { event: "onInstallEnded" }, + ], + }, + async () => { + const xpi2 = AddonTestUtils.createTempWebExtensionFile({ + manifest: ADDONS.test_install1_1, + }); + let install = await AddonManager.getInstallForFile(xpi2); + await install.install(); + } + ); + + addon = await promiseAddonByID(ID); + notEqual(addon, null); + equal(addon.pendingOperations, AddonManager.PENDING_NONE); + ok(addon.isActive); + ok(!addon.userDisabled); + + await expectEvents( + { + ignorePlugins: true, + addonEvents: { + [ID]: [{ event: "onUninstalling" }, { event: "onUninstalled" }], + }, + }, + () => addon.uninstall() + ); + + addon = await promiseAddonByID(ID); + equal(addon, null); + + await promiseRestartManager(); +}); + +// User intentionally reinstalls existing disabled addon of different version, +// but addon *still should be disabled*. +add_task(async function reinstallExistingDisabledAddonDifferentVersion() { + await expectEvents( + { + ignorePlugins: true, + addonEvents: { + [ID]: [{ event: "onInstalling" }, { event: "onInstalled" }], + }, + installEvents: [ + { event: "onNewInstall" }, + { event: "onInstallStarted" }, + { event: "onInstallEnded" }, + ], + }, + async () => { + const xpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: ADDONS.test_install1_1, + }); + let install = await AddonManager.getInstallForFile(xpi); + + await install.install(); + } + ); + + let addon = await promiseAddonByID(ID); + notEqual(addon, null); + equal(addon.pendingOperations, AddonManager.PENDING_NONE); + ok(addon.isActive); + ok(!addon.userDisabled); + + await expectEvents( + { + ignorePlugins: true, + addonEvents: { + [ID]: [{ event: "onDisabling" }, { event: "onDisabled" }], + }, + }, + () => addon.disable() + ); + + addon = await promiseAddonByID(ID); + notEqual(addon, null); + equal(addon.pendingOperations, AddonManager.PENDING_NONE); + ok(!addon.isActive); + ok(addon.userDisabled); + + await expectEvents( + { + ignorePlugins: true, + addonEvents: { + [ID]: [{ event: "onInstalling" }, { event: "onInstalled" }], + }, + installEvents: [ + { event: "onNewInstall" }, + { event: "onInstallStarted" }, + { event: "onInstallEnded" }, + ], + }, + async () => { + let xpi2 = AddonTestUtils.createTempWebExtensionFile({ + manifest: ADDONS.test_install1_2, + }); + let install = await AddonManager.getInstallForFile(xpi2); + await install.install(); + } + ); + + addon = await promiseAddonByID(ID); + notEqual(addon, null); + equal(addon.pendingOperations, AddonManager.PENDING_NONE); + ok(!addon.isActive); + ok(addon.userDisabled); + equal(addon.version, "2.0"); + + await expectEvents( + { + ignorePlugins: true, + addonEvents: { + [ID]: [{ event: "onUninstalling" }, { event: "onUninstalled" }], + }, + }, + () => addon.uninstall() + ); + + addon = await promiseAddonByID(ID); + equal(addon, null); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_reload.js b/toolkit/mozapps/extensions/test/xpcshell/test_reload.js new file mode 100644 index 0000000000..993c4a9c53 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_reload.js @@ -0,0 +1,188 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +const ID = "webextension1@tests.mozilla.org"; + +const ADDONS = { + webextension_1: { + "manifest.json": { + name: "Web Extension Name", + version: "1.0", + manifest_version: 2, + browser_specific_settings: { + gecko: { + id: ID, + }, + }, + icons: { + 48: "icon48.png", + 64: "icon64.png", + }, + }, + "chrome.manifest": "content webex ./\n", + }, +}; + +async function tearDownAddon(addon) { + await addon.uninstall(); + await promiseShutdownManager(); +} + +add_task(async function test_reloading_a_temp_addon() { + await promiseRestartManager(); + let xpi = AddonTestUtils.createTempXPIFile(ADDONS.webextension_1); + const addon = await AddonManager.installTemporaryAddon(xpi); + + var receivedOnUninstalled = false; + var receivedOnUninstalling = false; + var receivedOnInstalled = false; + var receivedOnInstalling = false; + + const onReload = new Promise(resolve => { + const listener = { + onUninstalling: addonObj => { + if (addonObj.id === ID) { + receivedOnUninstalling = true; + } + }, + onUninstalled: addonObj => { + if (addonObj.id === ID) { + receivedOnUninstalled = true; + } + }, + onInstalling: addonObj => { + receivedOnInstalling = true; + equal(addonObj.id, ID); + }, + onInstalled: addonObj => { + receivedOnInstalled = true; + equal(addonObj.id, ID); + // This should be the last event called. + AddonManager.removeAddonListener(listener); + resolve(); + }, + }; + AddonManager.addAddonListener(listener); + }); + + await addon.reload(); + await onReload; + + // Make sure reload() doesn't trigger uninstall events. + equal( + receivedOnUninstalled, + false, + "reload should not trigger onUninstalled" + ); + equal( + receivedOnUninstalling, + false, + "reload should not trigger onUninstalling" + ); + + // Make sure reload() triggers install events, like an upgrade. + equal(receivedOnInstalling, true, "reload should trigger onInstalling"); + equal(receivedOnInstalled, true, "reload should trigger onInstalled"); + + await tearDownAddon(addon); +}); + +add_task(async function test_can_reload_permanent_addon() { + await promiseRestartManager(); + const { addon } = await AddonTestUtils.promiseInstallXPI( + ADDONS.webextension_1 + ); + + let disabledCalled = false; + let enabledCalled = false; + AddonManager.addAddonListener({ + onDisabled: aAddon => { + Assert.ok(!enabledCalled); + disabledCalled = true; + }, + onEnabled: aAddon => { + Assert.ok(disabledCalled); + enabledCalled = true; + }, + }); + + await addon.reload(); + + Assert.ok(disabledCalled); + Assert.ok(enabledCalled); + + notEqual(addon, null); + equal(addon.appDisabled, false); + equal(addon.userDisabled, false); + + await tearDownAddon(addon); +}); + +add_task(async function test_reload_to_invalid_version_fails() { + await promiseRestartManager(); + let tempdir = gTmpD.clone(); + + // The initial version of the add-on will be compatible, and will therefore load + const addonId = "invalid_version_cannot_be_reloaded@tests.mozilla.org"; + let manifest = { + name: "invalid_version_cannot_be_reloaded", + description: "test invalid_version_cannot_be_reloaded", + manifest_version: 2, + version: "1.0", + browser_specific_settings: { + gecko: { + id: addonId, + }, + }, + }; + + let addonDir = await promiseWriteWebManifestForExtension( + manifest, + tempdir, + "invalid_version" + ); + await AddonManager.installTemporaryAddon(addonDir); + + let addon = await promiseAddonByID(addonId); + notEqual(addon, null); + equal(addon.id, addonId); + equal(addon.version, "1.0"); + equal(addon.appDisabled, false); + equal(addon.userDisabled, false); + addonDir.remove(true); + + // update the manifest to make the add-on version incompatible, so the reload will reject + manifest.browser_specific_settings.gecko.strict_min_version = "1"; + manifest.browser_specific_settings.gecko.strict_max_version = "1"; + manifest.version = "2.0"; + + addonDir = await promiseWriteWebManifestForExtension( + manifest, + tempdir, + "invalid_version", + false + ); + let expectedMsg = new RegExp( + "Add-on invalid_version_cannot_be_reloaded@tests.mozilla.org is not compatible with application version. " + + "add-on minVersion: 1. add-on maxVersion: 1." + ); + + await Assert.rejects( + addon.reload(), + expectedMsg, + "Reload rejects when application version does not fall between minVersion and maxVersion" + ); + + let reloadedAddon = await promiseAddonByID(addonId); + notEqual(reloadedAddon, null); + equal(reloadedAddon.id, addonId); + equal(reloadedAddon.version, "1.0"); + equal(reloadedAddon.appDisabled, false); + equal(reloadedAddon.userDisabled, false); + + await tearDownAddon(reloadedAddon); + addonDir.remove(true); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_remote_pref_telemetry.js b/toolkit/mozapps/extensions/test/xpcshell/test_remote_pref_telemetry.js new file mode 100644 index 0000000000..aeeb368aa7 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_remote_pref_telemetry.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +AddonTestUtils.init(this); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +// Need a profile dir to initialize Glean. +add_setup(async () => { + do_get_profile(); + Services.fog.initializeFOG(); +}); + +add_task(async function test_remote_extensions_pref_telemetry() { + let original = Services.prefs.getBoolPref("extensions.webextensions.remote"); + await AddonTestUtils.promiseStartupManager(); + + equal( + original, + Glean.extensions.useRemotePref.testGetValue(), + "useRemotePref flag in glean is correct." + ); + equal( + original, + Glean.extensions.useRemotePolicy.testGetValue(), + "useRemotePolicy flag in glean is correct." + ); + + // Change the pref to simulate nimbus doing so after startup. + Services.prefs.setBoolPref("extensions.webextensions.remote", !original); + + equal( + !original, + Glean.extensions.useRemotePref.testGetValue(), + "useRemotePref flag reflects the changed pref." + ); + // EPS::UseRemoteExtensions() only reads the pref once, for consistency. + equal( + original, + Glean.extensions.useRemotePolicy.testGetValue(), + "useRemotePolicy flag still equal to original pref value." + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_safemode.js b/toolkit/mozapps/extensions/test/xpcshell/test_safemode.js new file mode 100644 index 0000000000..30f4564e09 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_safemode.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that extensions behave correctly in safe mode +let scopes = AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION; +Services.prefs.setIntPref("extensions.enabledScopes", scopes); + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +const ID = "addon1@tests.mozilla.org"; +const BUILTIN_ID = "builtin@tests.mozilla.org"; +const VERSION = "1.0"; + +// Sets up the profile by installing an add-on. +add_task(async function setup() { + AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "1.9.2" + ); + gAppInfo.inSafeMode = true; + + await promiseStartupManager(); + + let a1 = await AddonManager.getAddonByID(ID); + Assert.equal(a1, null); + do_check_not_in_crash_annotation(ID, VERSION); + + await promiseInstallWebExtension({ + manifest: { + name: "Test 1", + version: VERSION, + browser_specific_settings: { gecko: { id: ID } }, + }, + }); + let wrapper = await installBuiltinExtension({ + manifest: { + browser_specific_settings: { gecko: { id: BUILTIN_ID } }, + }, + }); + + let builtin = await AddonManager.getAddonByID(BUILTIN_ID); + Assert.notEqual(builtin, null, "builtin extension is installed"); + + await promiseRestartManager(); + + a1 = await AddonManager.getAddonByID(ID); + Assert.notEqual(a1, null); + Assert.ok(!a1.isActive); + Assert.ok(!a1.userDisabled); + Assert.ok(isExtensionInBootstrappedList(profileDir, ID)); + Assert.ok(hasFlag(a1.permissions, AddonManager.PERM_CAN_DISABLE)); + Assert.ok(!hasFlag(a1.permissions, AddonManager.PERM_CAN_ENABLE)); + do_check_not_in_crash_annotation(ID, VERSION); + + builtin = await AddonManager.getAddonByID(BUILTIN_ID); + Assert.notEqual(builtin, null, "builtin extension is installed"); + Assert.ok(builtin.isActive, "builtin extension is active"); + await wrapper.unload(); +}); + +// Disabling an add-on should work +add_task(async function test_disable() { + let a1 = await AddonManager.getAddonByID(ID); + Assert.ok( + !hasFlag( + a1.operationsRequiringRestart, + AddonManager.OP_NEEDS_RESTART_DISABLE + ) + ); + await a1.disable(); + Assert.ok(!a1.isActive); + Assert.ok(!hasFlag(a1.permissions, AddonManager.PERM_CAN_DISABLE)); + Assert.ok(hasFlag(a1.permissions, AddonManager.PERM_CAN_ENABLE)); + do_check_not_in_crash_annotation(ID, VERSION); +}); + +// Enabling an add-on should happen but not become active. +add_task(async function test_enable() { + let a1 = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + await a1.enable(); + Assert.ok(!a1.isActive); + Assert.ok(hasFlag(a1.permissions, AddonManager.PERM_CAN_DISABLE)); + Assert.ok(!hasFlag(a1.permissions, AddonManager.PERM_CAN_ENABLE)); + + do_check_not_in_crash_annotation(ID, VERSION); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_schema_change.js b/toolkit/mozapps/extensions/test/xpcshell/test_schema_change.js new file mode 100644 index 0000000000..591bf6eb56 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_schema_change.js @@ -0,0 +1,157 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const PREF_DB_SCHEMA = "extensions.databaseSchema"; +const PREF_IS_EMBEDDED = "extensions.isembedded"; + +registerCleanupFunction(() => { + Services.prefs.clearUserPref(PREF_IS_EMBEDDED); +}); + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49"); + +add_task(async function test_setup() { + await promiseStartupManager(); +}); + +add_task(async function run_tests() { + // Fake installTelemetryInfo used in the addon installation, + // to verify that they are preserved after the DB is updated + // from the addon manifests. + const fakeInstallTelemetryInfo = { source: "amo", method: "amWebAPI" }; + + const ID = "schema-change@tests.mozilla.org"; + + const xpi1 = createTempWebExtensionFile({ + manifest: { + name: "Test Add-on", + version: "1.0", + browser_specific_settings: { gecko: { id: ID } }, + }, + }); + + const xpi2 = createTempWebExtensionFile({ + manifest: { + name: "Test Add-on 2", + version: "2.0", + browser_specific_settings: { gecko: { id: ID } }, + }, + }); + + let xpiPath = PathUtils.join(profileDir.path, `${ID}.xpi`); + + const TESTS = [ + { + what: "Schema change with no application update reloads metadata.", + expectedVersion: "2.0", + action() { + Services.prefs.setIntPref(PREF_DB_SCHEMA, 0); + }, + }, + { + what: "Application update with no schema change does not reload metadata.", + expectedVersion: "1.0", + action() { + gAppInfo.version = "2"; + }, + }, + { + what: "App update and a schema change causes a reload of the manifest.", + expectedVersion: "2.0", + action() { + gAppInfo.version = "3"; + Services.prefs.setIntPref(PREF_DB_SCHEMA, 0); + }, + }, + { + what: "No schema change, no manifest reload.", + expectedVersion: "1.0", + action() {}, + }, + { + what: "Modified timestamp on the XPI causes a reload of the manifest.", + expectedVersion: "2.0", + async action() { + let stat = await IOUtils.stat(xpiPath); + let newLastModTime = stat.lastModified + 60 * 1000; + await IOUtils.setModificationTime(xpiPath, newLastModTime); + }, + }, + ]; + + for (let test of TESTS) { + info(test.what); + await promiseInstallFile(xpi1, false, fakeInstallTelemetryInfo); + + let addon = await promiseAddonByID(ID); + notEqual(addon, null, "Got an addon object as expected"); + equal(addon.version, "1.0", "Got the expected version"); + Assert.deepEqual( + addon.installTelemetryInfo, + fakeInstallTelemetryInfo, + "Got the expected installTelemetryInfo after installing the addon" + ); + + await promiseShutdownManager(); + + let fileInfo = await IOUtils.stat(xpiPath); + + xpi2.copyTo(profileDir, `${ID}.xpi`); + + // Make sure the timestamp of the extension is unchanged, so it is not + // re-scanned for that reason. + await IOUtils.setModificationTime(xpiPath, fileInfo.lastModified); + + await test.action(); + + await promiseStartupManager(); + + addon = await promiseAddonByID(ID); + notEqual(addon, null, "Got an addon object as expected"); + equal(addon.version, test.expectedVersion, "Got the expected version"); + Assert.deepEqual( + addon.installTelemetryInfo, + fakeInstallTelemetryInfo, + "Got the expected installTelemetryInfo after rebuilding the DB" + ); + + await addon.uninstall(); + } +}); + +add_task(async function embedder_disabled_stays_disabled() { + Services.prefs.setBoolPref(PREF_IS_EMBEDDED, true); + + const ID = "embedder-disabled@tests.mozilla.org"; + + await promiseInstallWebExtension({ + manifest: { + name: "Test Add-on", + version: "1.0", + browser_specific_settings: { gecko: { id: ID } }, + }, + }); + + let addon = await promiseAddonByID(ID); + + equal(addon.embedderDisabled, false); + + await addon.setEmbedderDisabled(true); + equal(addon.embedderDisabled, true); + + await promiseShutdownManager(); + + // Change db schema to force reload + Services.prefs.setIntPref(PREF_DB_SCHEMA, 0); + + await promiseStartupManager(); + + addon = await promiseAddonByID(ID); + equal(addon.embedderDisabled, true); + + await addon.uninstall(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_seen.js b/toolkit/mozapps/extensions/test/xpcshell/test_seen.js new file mode 100644 index 0000000000..fbf43f5cc0 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_seen.js @@ -0,0 +1,277 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const ID = "addon@tests.mozilla.org"; + +let profileDir = gProfD.clone(); +profileDir.append("extensions"); + +// By default disable add-ons from the profile and the system-wide scope +const SCOPES = AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_SYSTEM; +Services.prefs.setIntPref("extensions.enabledScopes", SCOPES); +Services.prefs.setIntPref("extensions.autoDisableScopes", SCOPES); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + +const XPIS = {}; + +// Installing an add-on through the API should mark it as seen +add_task(async function () { + await promiseStartupManager(); + + for (let n of [1, 2]) { + XPIS[n] = await createTempWebExtensionFile({ + manifest: { + name: "Test", + version: `${n}.0`, + browser_specific_settings: { gecko: { id: ID } }, + }, + }); + } + + await promiseInstallFile(XPIS[1]); + + let addon = await promiseAddonByID(ID); + Assert.equal(addon.version, "1.0"); + Assert.ok(!addon.foreignInstall); + Assert.ok(addon.seen); + + await promiseRestartManager(); + + addon = await promiseAddonByID(ID); + Assert.ok(!addon.foreignInstall); + Assert.ok(addon.seen); + + // Installing an update should retain that + await promiseInstallFile(XPIS[2]); + + addon = await promiseAddonByID(ID); + Assert.equal(addon.version, "2.0"); + Assert.ok(!addon.foreignInstall); + Assert.ok(addon.seen); + + await promiseRestartManager(); + + addon = await promiseAddonByID(ID); + Assert.ok(!addon.foreignInstall); + Assert.ok(addon.seen); + + await addon.uninstall(); + + await promiseShutdownManager(); +}); + +// Sideloading an add-on in the systemwide location should mark it as unseen +add_task(async function () { + let savedStartupScanScopes = Services.prefs.getIntPref( + "extensions.startupScanScopes" + ); + Services.prefs.setIntPref("extensions.startupScanScopes", 0); + Services.prefs.setIntPref( + "extensions.sideloadScopes", + AddonManager.SCOPE_ALL + ); + + let systemParentDir = gTmpD.clone(); + systemParentDir.append("systemwide-extensions"); + registerDirectory("XRESysSExtPD", systemParentDir.clone()); + registerCleanupFunction(() => { + systemParentDir.remove(true); + }); + + let systemDir = systemParentDir.clone(); + systemDir.append(Services.appinfo.ID); + + let path = await manuallyInstall(XPIS[1], systemDir, ID); + // Make sure the startup code will detect sideloaded updates + setExtensionModifiedTime(path, Date.now() - 10000); + + await promiseStartupManager(); + await AddonManagerPrivate.getNewSideloads(); + + let addon = await promiseAddonByID(ID); + Assert.equal(addon.version, "1.0"); + Assert.ok(addon.foreignInstall); + Assert.ok(!addon.seen); + + await promiseRestartManager(); + + addon = await promiseAddonByID(ID); + Assert.ok(addon.foreignInstall); + Assert.ok(!addon.seen); + + await promiseShutdownManager(); + Services.obs.notifyObservers(path, "flush-cache-entry"); + path.remove(true); + + Services.prefs.setIntPref( + "extensions.startupScanScopes", + savedStartupScanScopes + ); + Services.prefs.clearUserPref("extensions.sideloadScopes"); +}); + +// Sideloading an add-on in the profile should mark it as unseen and it should +// remain unseen after an update is sideloaded. +add_task(async function () { + let path = await manuallyInstall(XPIS[1], profileDir, ID); + // Make sure the startup code will detect sideloaded updates + setExtensionModifiedTime(path, Date.now() - 10000); + + await promiseStartupManager(); + + let addon = await promiseAddonByID(ID); + Assert.equal(addon.version, "1.0"); + Assert.ok(addon.foreignInstall); + Assert.ok(!addon.seen); + + await promiseRestartManager(); + + addon = await promiseAddonByID(ID); + Assert.ok(addon.foreignInstall); + Assert.ok(!addon.seen); + + await promiseShutdownManager(); + + // Sideloading an update shouldn't change the state + manuallyUninstall(profileDir, ID); + await manuallyInstall(XPIS[2], profileDir, ID); + setExtensionModifiedTime(path, Date.now()); + + await promiseStartupManager(); + + addon = await promiseAddonByID(ID); + Assert.equal(addon.version, "2.0"); + Assert.ok(addon.foreignInstall); + Assert.ok(!addon.seen); + + await addon.uninstall(); + await promiseShutdownManager(); +}); + +// Sideloading an add-on in the profile should mark it as unseen and it should +// remain unseen after a regular update. +add_task(async function () { + let path = await manuallyInstall(XPIS[1], profileDir, ID); + // Make sure the startup code will detect sideloaded updates + setExtensionModifiedTime(path, Date.now() - 10000); + + await promiseStartupManager(); + + let addon = await promiseAddonByID(ID); + Assert.equal(addon.version, "1.0"); + Assert.ok(addon.foreignInstall); + Assert.ok(!addon.seen); + + await promiseRestartManager(); + + addon = await promiseAddonByID(ID); + Assert.ok(addon.foreignInstall); + Assert.ok(!addon.seen); + + // Updating through the API shouldn't change the state + let install = await promiseInstallFile(XPIS[2]); + Assert.equal(install.state, AddonManager.STATE_INSTALLED); + Assert.ok( + !hasFlag(install.addon.pendingOperations, AddonManager.PENDING_INSTALL) + ); + + addon = install.addon; + Assert.ok(addon.foreignInstall); + Assert.ok(!addon.seen); + + await promiseRestartManager(); + + addon = await promiseAddonByID(ID); + Assert.equal(addon.version, "2.0"); + Assert.ok(addon.foreignInstall); + Assert.ok(!addon.seen); + + await addon.uninstall(); + await promiseShutdownManager(); +}); + +// After a sideloaded addon has been seen, sideloading an update should +// not reset it to unseen. +add_task(async function () { + let path = await manuallyInstall(XPIS[1], profileDir, ID); + // Make sure the startup code will detect sideloaded updates + setExtensionModifiedTime(path, Date.now() - 10000); + + await promiseStartupManager(); + + let addon = await promiseAddonByID(ID); + Assert.equal(addon.version, "1.0"); + Assert.ok(addon.foreignInstall); + Assert.ok(!addon.seen); + addon.markAsSeen(); + Assert.ok(addon.seen); + + await promiseRestartManager(); + + addon = await promiseAddonByID(ID); + Assert.ok(addon.foreignInstall); + Assert.ok(addon.seen); + + await promiseShutdownManager(); + + // Sideloading an update shouldn't change the state + manuallyUninstall(profileDir, ID); + await manuallyInstall(XPIS[2], profileDir, ID); + setExtensionModifiedTime(path, Date.now()); + + await promiseStartupManager(); + + addon = await promiseAddonByID(ID); + Assert.equal(addon.version, "2.0"); + Assert.ok(addon.foreignInstall); + Assert.ok(addon.seen); + + await addon.uninstall(); + await promiseShutdownManager(); +}); + +// After a sideloaded addon has been seen, manually applying an update should +// not reset it to unseen. +add_task(async function () { + let path = await manuallyInstall(XPIS[1], profileDir, ID); + // Make sure the startup code will detect sideloaded updates + setExtensionModifiedTime(path, Date.now() - 10000); + + await promiseStartupManager(); + + let addon = await promiseAddonByID(ID); + Assert.equal(addon.version, "1.0"); + Assert.ok(addon.foreignInstall); + Assert.ok(!addon.seen); + addon.markAsSeen(); + Assert.ok(addon.seen); + + await promiseRestartManager(); + + addon = await promiseAddonByID(ID); + Assert.ok(addon.foreignInstall); + Assert.ok(addon.seen); + + // Updating through the API shouldn't change the state + let install = await promiseInstallFile(XPIS[2]); + Assert.equal(install.state, AddonManager.STATE_INSTALLED); + Assert.ok( + !hasFlag(install.addon.pendingOperations, AddonManager.PENDING_INSTALL) + ); + + addon = install.addon; + Assert.ok(addon.foreignInstall); + Assert.ok(addon.seen); + + await promiseRestartManager(); + + addon = await promiseAddonByID(ID); + Assert.equal(addon.version, "2.0"); + Assert.ok(addon.foreignInstall); + Assert.ok(addon.seen); + + await addon.uninstall(); + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js new file mode 100644 index 0000000000..d6fe082666 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js @@ -0,0 +1,131 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Verify that API functions fail if the Add-ons Manager isn't initialised. + +const IGNORE = [ + "getPreferredIconURL", + "escapeAddonURI", + "shouldAutoUpdate", + "getStartupChanges", + "addAddonListener", + "removeAddonListener", + "addInstallListener", + "removeInstallListener", + "addManagerListener", + "removeManagerListener", + "addExternalExtensionLoader", + "beforeShutdown", + "init", + "stateToString", + "errorToString", + "getUpgradeListener", + "addUpgradeListener", + "removeUpgradeListener", + "getInstallSourceFromHost", + "stageLangpacksForAppUpdate", +]; + +const IGNORE_PRIVATE = [ + "AddonAuthor", + "AddonScreenshot", + "startup", + "shutdown", + "addonIsActive", + "registerProvider", + "unregisterProvider", + "addStartupChange", + "removeStartupChange", + "getNewSideloads", + "finalShutdown", + "recordTimestamp", + "recordSimpleMeasure", + "recordException", + "getSimpleMeasures", + "simpleTimer", + "setTelemetryDetails", + "getTelemetryDetails", + "callNoUpdateListeners", + "backgroundUpdateTimerHandler", + "hasUpgradeListener", + "getUpgradeListener", + "isDBLoaded", + "recordTiming", + "BOOTSTRAP_REASONS", + "notifyAddonChanged", + "overrideAddonRepository", + "overrideAsyncShutdown", +]; + +async function test_functions() { + for (let prop in AddonManager) { + if (IGNORE.includes(prop)) { + continue; + } + if (typeof AddonManager[prop] != "function") { + continue; + } + + let args = []; + + // Getter functions need a callback and in some cases not having one will + // throw before checking if the add-ons manager is initialized so pass in + // an empty one. + if (prop.startsWith("get")) { + // For now all getter functions with more than one argument take the + // callback in the second argument. + if (AddonManager[prop].length > 1) { + args.push(undefined, () => {}); + } else { + args.push(() => {}); + } + } + + // Clean this up in bug 1365720 + if (prop == "getActiveAddons") { + args = []; + } + + try { + info("AddonManager." + prop); + await AddonManager[prop](...args); + do_throw(prop + " did not throw an exception"); + } catch (e) { + if (e.result != Cr.NS_ERROR_NOT_INITIALIZED) { + do_throw(prop + " threw an unexpected exception: " + e); + } + } + } + + for (let prop in AddonManagerPrivate) { + if (IGNORE_PRIVATE.includes(prop)) { + continue; + } + if (typeof AddonManagerPrivate[prop] != "function") { + continue; + } + + try { + info("AddonManagerPrivate." + prop); + AddonManagerPrivate[prop](); + do_throw(prop + " did not throw an exception"); + } catch (e) { + if (e.result != Cr.NS_ERROR_NOT_INITIALIZED) { + do_throw(prop + " threw an unexpected exception: " + e); + } + } + } +} + +add_task(async function () { + await test_functions(); + await promiseStartupManager(); + await promiseShutdownManager(); + await test_functions(); +}); + +function run_test() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + run_next_test(); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_shutdown_barriers.js b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown_barriers.js new file mode 100644 index 0000000000..9fd06bb5ee --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown_barriers.js @@ -0,0 +1,215 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + Management: "resource://gre/modules/Extension.sys.mjs", +}); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +add_task(async function test_shutdown_barriers() { + await promiseStartupManager(); + + const ID = "thing@xpcshell.addons.mozilla.org"; + const VERSION = "1.42"; + + let xpi = await createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { gecko: { id: ID } }, + version: VERSION, + }, + }); + + await AddonManager.installTemporaryAddon(xpi); + + let blockersComplete = 0; + AddonManager.beforeShutdown.addBlocker("Before shutdown", async () => { + equal(blockersComplete, 0, "No blockers have run yet"); + + // Delay a bit to make sure this doesn't succeed because of a timing + // fluke. + await delay(1000); + + let addon = await AddonManager.getAddonByID(ID); + checkAddon(ID, addon, { + version: VERSION, + userDisabled: false, + appDisabled: false, + }); + + await addon.disable(); + + // Delay again for the same reasons. + await delay(1000); + + addon = await AddonManager.getAddonByID(ID); + checkAddon(ID, addon, { + version: VERSION, + userDisabled: true, + appDisabled: false, + }); + + blockersComplete++; + }); + AddonManagerPrivate.finalShutdown.addBlocker("Before shutdown", async () => { + equal(blockersComplete, 1, "Before shutdown blocker has run"); + + // Should probably try to access XPIDatabase here to make sure it + // doesn't work correctly, but it's a bit hairy. + + // Delay a bit to make sure this doesn't succeed because of a timing + // fluke. + await delay(1000); + + blockersComplete++; + }); + + await promiseShutdownManager(); + equal( + blockersComplete, + 2, + "Both shutdown blockers ran before manager shutdown completed" + ); +}); + +// Regression test for Bug 1814104. +add_task(async function test_wait_addons_startup_before_granting_quit() { + await promiseStartupManager(); + + const extensions = []; + for (let i = 0; i < 20; i++) { + extensions.push( + ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + background() {}, + manifest: { + browser_specific_settings: { + gecko: { id: `test-extension-${i}@mozilla` }, + }, + }, + }) + ); + } + + info("Wait for all test extension to have been started once"); + await Promise.all(extensions.map(ext => ext.startup())); + await promiseShutdownManager(); + + info("Test early shutdown while enabled addons are still being started"); + const { XPIExports } = ChromeUtils.importESModule( + "resource://gre/modules/addons/XPIExports.sys.mjs" + ); + function listener(_evt, extension) { + ok( + !XPIExports.XPIProvider._closing, + `Unxpected addon startup for "${extension.id}" after XPIProvider have been closed and shutting down` + ); + } + Management.on("startup", listener); + promiseStartupManager(); + await promiseShutdownManager(); + + info("Uninstall test extensions"); + Management.off("startup", listener); + await promiseStartupManager(); + await Promise.all(extensions.map(ext => ext.awaitStartup)); + await Promise.all( + extensions.map(ext => { + return AddonManager.getAddonByID(ext.id).then(addon => + addon?.uninstall() + ); + }) + ); + await promiseShutdownManager(); +}); + +// Regression test for Bug 1799421. +add_task(async function test_late_XPIDB_load_rejected() { + const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" + ); + const sandbox = sinon.createSandbox(); + await AddonTestUtils.promiseStartupManager(); + + // Mock a late XPIDB load and expect to be rejected. + const { XPIExports } = ChromeUtils.importESModule( + "resource://gre/modules/addons/XPIExports.sys.mjs" + ); + + const resolveDBReadySpy = sinon.spy(XPIExports.XPIInternal, "resolveDBReady"); + XPIExports.XPIProvider._closing = true; + XPIExports.XPIDatabase._dbPromise = null; + + Assert.equal( + await XPIExports.XPIDatabase.getAddonByID("test@addon"), + null, + "Expect a late getAddonByID call to be sucessfully resolved to null" + ); + + await Assert.rejects( + XPIExports.XPIDatabase._dbPromise, + /XPIDatabase.asyncLoadDB attempt after XPIProvider shutdown/, + "Expect XPIDatebase._dbPromise to be set to the expected rejected promise" + ); + + Assert.equal( + resolveDBReadySpy.callCount, + 1, + "Expect resolveDBReadySpy to have been called once" + ); + + Assert.equal( + resolveDBReadySpy.getCall(0).args[0], + XPIExports.XPIDatabase._dbPromise, + "Got the expected promise instance passed to the XPIProvider.resolveDBReady call" + ); + + // Cleanup sinon spy, AOM mocked status and shutdown AOM before exit test. + sandbox.restore(); + XPIExports.XPIProvider._closing = false; + XPIExports.XPIDatabase._dbPromise = null; + await AddonTestUtils.promiseShutdownManager(); +}); + +// Regression test for Bug 1799421. +// +// NOTE: this test calls Services.startup.advanceShutdownPhase +// with SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED to mock the scenario +// and so using promiseStartupManager/promiseShutdownManager will +// fail to bring the AddonManager back into a working status +// because it would be detected as too late on the shutdown path. +add_task(async function test_late_bootstrapscope_startup_rejected() { + await AddonTestUtils.promiseStartupManager(); + + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + browser_specific_settings: { + gecko: { id: "test@addon" }, + }, + }, + }); + + await extension.startup(); + const { addon } = extension; + await addon.disable(); + // Mock a shutdown which already got to shutdown confirmed + // and expect a rejection from trying to startup the BootstrapScope + // too late on shutdown. + Services.startup.advanceShutdownPhase( + Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED + ); + + await Assert.rejects( + addon.enable(), + /XPIProvider can't start bootstrap scope for test@addon after shutdown was already granted/, + "Got the expected rejection on trying to enable the extension after shutdown granted" + ); + + info("Cleanup mocked late bootstrap scope before exit test"); + await AddonTestUtils.promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_shutdown_early.js b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown_early.js new file mode 100644 index 0000000000..2af6d42365 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown_early.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +const { TelemetryEnvironment } = ChromeUtils.importESModule( + "resource://gre/modules/TelemetryEnvironment.sys.mjs" +); + +// Regression test for bug 1665568: verifies that AddonManager unblocks shutdown +// when startup is interrupted very early. +add_task(async function test_shutdown_immediately_after_startup() { + // Set as migrated to prevent sync DB load at startup. + Services.prefs.setCharPref("extensions.lastAppVersion", "42"); + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42"); + + Cc["@mozilla.org/addons/integration;1"] + .getService(Ci.nsIObserver) + .observe(null, "addons-startup", null); + + // Above, we have configured the runtime to avoid a forced synchronous load + // of the database. Confirm that this is indeed the case. + equal(AddonManagerPrivate.isDBLoaded(), false, "DB not loaded synchronously"); + + let shutdownCount = 0; + AddonManager.beforeShutdown.addBlocker("count", async () => ++shutdownCount); + + let databaseLoaded = false; + AddonManagerPrivate.databaseReady.then(() => { + databaseLoaded = true; + }); + + // Accessing TelemetryEnvironment.currentEnvironment triggers initialization + // of TelemetryEnvironment / EnvironmentAddonBuilder, which registers a + // shutdown blocker. + equal( + TelemetryEnvironment.currentEnvironment.addons, + undefined, + "TelemetryEnvironment.currentEnvironment.addons is uninitialized" + ); + + info("Immediate exit at startup, without quit-application-granted"); + Services.startup.advanceShutdownPhase( + Services.startup.SHUTDOWN_PHASE_APPSHUTDOWN + ); + let shutdownPromise = MockAsyncShutdown.profileBeforeChange.trigger(); + equal(shutdownCount, 1, "AddonManager.beforeShutdown has started"); + + // Note: Until now everything ran in the same tick of the event loop. + + // Waiting for AddonManager to have shut down. + await shutdownPromise; + + ok(databaseLoaded, "Addon DB loaded for use by TelemetryEnvironment"); + equal(AddonManagerPrivate.isDBLoaded(), false, "DB unloaded after shutdown"); + + Assert.deepEqual( + TelemetryEnvironment.currentEnvironment.addons.activeAddons, + {}, + "TelemetryEnvironment.currentEnvironment.addons is initialized" + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_sideload_scopes.js b/toolkit/mozapps/extensions/test/xpcshell/test_sideload_scopes.js new file mode 100644 index 0000000000..6631fa47c5 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_sideload_scopes.js @@ -0,0 +1,188 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +// We refer to addons that were sideloaded prior to disabling sideloading as legacy. We +// determine that they are legacy because they are in a SCOPE that is not included +// in AddonSettings.SCOPES_SIDELOAD. +// +// test_startup.js tests the legacy sideloading functionality still works as it should +// for ESR and some 3rd party distributions, which is to allow sideloading in all scopes. +// +// This file tests that locking down the sideload functionality works as we expect it to, which +// is to allow sideloading only in SCOPE_APPLICATION and SCOPE_PROFILE. We also allow the legacy +// sideloaded addons to be updated or removed. +// +// We first change the sideload scope so we can sideload some addons into locations outside +// the profile (ie. we create some legacy sideloads). We then reset it to test new sideloads. +// +// We expect new sideloads to only work in profile. +// We expect new sideloads to fail elsewhere. +// We expect to be able to change/uninstall legacy sideloads. + +// This test uses add-on versions that follow the toolkit version but we +// started to encourage the use of a simpler format in Bug 1793925. We disable +// the pref below to avoid install errors. +Services.prefs.setBoolPref( + "extensions.webextensions.warnings-as-errors", + false +); + +// IDs for scopes that should sideload when sideloading +// is not disabled. +let legacyIDs = [ + getID(`legacy-global`), + getID(`legacy-user`), + getID(`legacy-app`), + getID(`legacy-profile`), +]; + +add_task(async function test_sideloads_legacy() { + let IDs = []; + + // Create a "legacy" addon for each scope. + for (let [name, dir] of Object.entries(scopeDirectories)) { + let id = getID(`legacy-${name}`); + IDs.push(id); + await createWebExtension(id, initialVersion(name), dir); + } + + await promiseStartupManager(); + + // SCOPE_APPLICATION will never sideload, so we expect 3 addons. + let sideloaded = await AddonManagerPrivate.getNewSideloads(); + Assert.equal(sideloaded.length, 4, "four sideloaded addons"); + let sideloadedIds = sideloaded.map(a => a.id); + for (let id of legacyIDs) { + Assert.ok(sideloadedIds.includes(id), `${id} is sideloaded`); + } + + check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []); + + await promiseShutdownManager(); +}); + +// Test that a sideload install in SCOPE_PROFILE is allowed, all others are +// disallowed. +add_task(async function test_sideloads_disabled() { + // First, reset our scope pref to disable sideloading. head_sideload.js set this to ALL. + Services.prefs.setIntPref( + "extensions.sideloadScopes", + AddonManager.SCOPE_PROFILE + ); + + // Create 4 new addons, only one of these, "profile" should + // sideload. + for (let [name, dir] of Object.entries(scopeDirectories)) { + await createWebExtension(getID(name), initialVersion(name), dir); + } + + await promiseStartupManager(); + + // Test that the "profile" addon has been sideloaded. + let sideloaded = await AddonManagerPrivate.getNewSideloads(); + Assert.equal(sideloaded.length, 1, "one sideloaded addon"); + + check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, [ + getID("profile"), + ]); + check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []); + + for (let [name] of Object.entries(scopeDirectories)) { + let id = getID(name); + let addon = await promiseAddonByID(id); + if (name === "profile") { + Assert.notEqual(addon, null); + Assert.equal(addon.id, id); + Assert.ok(addon.foreignInstall); + Assert.equal(addon.scope, AddonManager.SCOPE_PROFILE); + Assert.ok(addon.userDisabled); + Assert.ok(!addon.seen); + } else { + Assert.equal(addon, null, `addon ${id} is not installed`); + } + } + + // Test that we still have the 3 legacy addons from the prior test, plus + // the new "profile" addon from this test. + let extensionAddons = await AddonManager.getAddonsByTypes(["extension"]); + Assert.equal( + extensionAddons.length, + 5, + "five addons expected to be installed" + ); + let IDs = extensionAddons.map(ext => ext.id); + for (let id of [getID("profile"), ...legacyIDs]) { + Assert.ok(IDs.includes(id), `${id} is installed`); + } + + await promiseShutdownManager(); +}); + +add_task(async function test_sideloads_changed() { + // Upgrade the manifest version + for (let [name, dir] of Object.entries(scopeDirectories)) { + let id = getID(name); + await createWebExtension(id, `${name}.1`, dir); + + id = getID(`legacy-${name}`); + await createWebExtension(id, `${name}.1`, dir); + } + + await promiseStartupManager(); + let addons = await AddonManager.getAddonsByTypes(["extension"]); + Assert.equal(addons.length, 5, "addons installed"); + + check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, [ + getID("profile"), + ...legacyIDs, + ]); + check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []); + + await promiseShutdownManager(); +}); + +// Remove one just to test the startup changes +add_task(async function test_sideload_removal() { + let id = getID(`legacy-profile`); + let file = AddonTestUtils.getFileForAddon(profileDir, id); + file.remove(false); + Assert.ok(!file.exists()); + + await promiseStartupManager(); + + check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, [id]); + + await promiseShutdownManager(); +}); + +add_task(async function test_sideload_uninstall() { + await promiseStartupManager(); + let addons = await AddonManager.getAddonsByTypes(["extension"]); + Assert.equal(addons.length, 4, "addons installed"); + for (let addon of addons) { + let file = AddonTestUtils.getFileForAddon( + scopeToDir.get(addon.scope), + addon.id + ); + await addon.uninstall(); + // Addon file should still exist in non-profile directories. + Assert.equal( + addon.scope !== AddonManager.SCOPE_PROFILE, + file.exists(), + `file remains after uninstall for non-profile sideloads, scope ${addon.scope}` + ); + } + addons = await AddonManager.getAddonsByTypes(["extension"]); + Assert.equal(addons.length, 0, "addons left"); + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_sideloads.js b/toolkit/mozapps/extensions/test/xpcshell/test_sideloads.js new file mode 100644 index 0000000000..f2ffe8e855 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_sideloads.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49"); + +const ID1 = "addon1@tests.mozilla.org"; +const ID2 = "addon2@tests.mozilla.org"; +const ID3 = "addon3@tests.mozilla.org"; + +async function createWebExtension(details) { + let options = { + manifest: { + browser_specific_settings: { gecko: { id: details.id } }, + + name: details.name, + + permissions: details.permissions, + }, + }; + + if (details.iconURL) { + options.manifest.icons = { 64: details.iconURL }; + } + + let xpi = AddonTestUtils.createTempWebExtensionFile(options); + + await AddonTestUtils.manuallyInstall(xpi); +} + +add_task(async function test_sideloading() { + Services.prefs.setIntPref("extensions.autoDisableScopes", 15); + Services.prefs.setIntPref("extensions.startupScanScopes", 0); + + await createWebExtension({ + id: ID1, + name: "Test 1", + userDisabled: true, + permissions: ["tabs", "https://*/*"], + iconURL: "foo-icon.png", + }); + + await createWebExtension({ + id: ID2, + name: "Test 2", + permissions: ["<all_urls>"], + }); + + await createWebExtension({ + id: ID3, + name: "Test 3", + permissions: ["<all_urls>"], + }); + + await promiseStartupManager(); + + let sideloaded = await AddonManagerPrivate.getNewSideloads(); + + sideloaded.sort((a, b) => a.id.localeCompare(b.id)); + + deepEqual( + sideloaded.map(a => a.id), + [ID1, ID2, ID3], + "Got the correct sideload add-ons" + ); + + deepEqual( + sideloaded.map(a => a.userDisabled), + [true, true, true], + "All sideloaded add-ons are disabled" + ); +}); + +add_task(async function test_getNewSideload_on_invalid_extension() { + let destDir = AddonTestUtils.profileExtensions.clone(); + + let xpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { gecko: { id: "@invalid-extension" } }, + name: "Invalid Extension", + }, + }); + + // Create an invalid sideload by creating a file name that doesn't match the + // actual extension id. + await IOUtils.copy( + xpi.path, + PathUtils.join(destDir.path, "@wrong-extension-filename.xpi") + ); + + // Verify that getNewSideloads does not reject or throw when one of the sideloaded extensions + // is invalid. + const newSideloads = await AddonManagerPrivate.getNewSideloads(); + + const sideloadsInfo = newSideloads + .sort((a, b) => a.id.localeCompare(b.id)) + .map(({ id, seen, userDisabled, permissions }) => { + return { + id, + seen, + userDisabled, + canEnable: Boolean(permissions & AddonManager.PERM_CAN_ENABLE), + }; + }); + + const expectedInfo = { seen: false, userDisabled: true, canEnable: true }; + + Assert.deepEqual( + sideloadsInfo, + [ + { id: ID1, ...expectedInfo }, + { id: ID2, ...expectedInfo }, + { id: ID3, ...expectedInfo }, + ], + "Got the expected sideloaded extensions" + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_sideloads_after_rebuild.js b/toolkit/mozapps/extensions/test/xpcshell/test_sideloads_after_rebuild.js new file mode 100644 index 0000000000..24aa8b228a --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_sideloads_after_rebuild.js @@ -0,0 +1,149 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +"use strict"; + +// This test uses add-on versions that follow the toolkit version but we +// started to encourage the use of a simpler format in Bug 1793925. We disable +// the pref below to avoid install errors. +Services.prefs.setBoolPref( + "extensions.webextensions.warnings-as-errors", + false +); + +// IDs for scopes that should sideload when sideloading +// is not disabled. +let legacyIDs = [ + getID(`legacy-global`), + getID(`legacy-user`), + getID(`legacy-app`), + getID(`legacy-profile`), +]; + +// This tests that, on a rebuild after addonStartup.json and extensions.json +// are lost, we only sideload from the profile. +add_task(async function test_sideloads_after_rebuild() { + let IDs = []; + + // Create a sideloaded addon for each scope before the restriction is put + // in place (by updating the sideloadScopes preference). + for (let [name, dir] of Object.entries(scopeDirectories)) { + let id = getID(`legacy-${name}`); + IDs.push(id); + await createWebExtension(id, initialVersion(name), dir); + } + + await promiseStartupManager(); + + // SCOPE_APPLICATION will never sideload, so we expect 3 + let sideloaded = await AddonManagerPrivate.getNewSideloads(); + Assert.equal(sideloaded.length, 4, "four sideloaded addon"); + let sideloadedIds = sideloaded.map(a => a.id); + for (let id of legacyIDs) { + Assert.ok(sideloadedIds.includes(id)); + } + + // After a restart that causes a database rebuild, we should have + // the same addons available + await promiseShutdownManager(); + // Reset our scope pref so the scope limitation works. + Services.prefs.setIntPref( + "extensions.sideloadScopes", + AddonManager.SCOPE_PROFILE + ); + + // Try to sideload from a non-profile directory. + await createWebExtension( + getID(`sideload-global-1`), + initialVersion("sideload-global"), + globalDir + ); + + await promiseStartupManager("2"); + + // We should still only have 4 addons. + let addons = await AddonManager.getAddonsByTypes(["extension"]); + Assert.equal(addons.length, 4, "addons remain installed"); + + await promiseShutdownManager(); + + // Install a sideload that will not load because it is not in + // appStartup.json and is not in a sideloadScope. + await createWebExtension( + getID(`sideload-global-2`), + initialVersion("sideload-global"), + globalDir + ); + await createWebExtension( + getID(`sideload-app-2`), + initialVersion("sideload-global"), + globalDir + ); + // Install a sideload that will load. We cannot currently prevent + // this situation. + await createWebExtension( + getID(`sideload-profile`), + initialVersion("sideload-profile"), + profileDir + ); + + // Replace the extensions.json with something bogus so we lose our xpidatabase. + // On AOM startup, addons are restored with help from XPIState. Existing + // sideloads should all remain. One new sideloaded addon should be added from + // the profile. + await IOUtils.writeJSON(gExtensionsJSON.path, { + not: "what we expect to find", + }); + info(`**** restart AOM and rebuild XPI database`); + await promiseStartupManager(); + + addons = await AddonManager.getAddonsByTypes(["extension"]); + Assert.equal(addons.length, 5, "addons installed"); + + await promiseShutdownManager(); + + // Install a sideload that will not load. + await createWebExtension( + getID(`sideload-global-3`), + initialVersion("sideload-global"), + globalDir + ); + // Install a sideload that will load. We cannot currently prevent + // this situation. + await createWebExtension( + getID(`sideload-profile-2`), + initialVersion("sideload-profile"), + profileDir + ); + + // Replace the extensions.json with something bogus so we lose our xpidatabase. + await IOUtils.writeJSON(gExtensionsJSON.path, { + not: "what we expect to find", + }); + // Delete our appStartup/XPIState data. Now we should only be able to + // restore extensions in the profile. + gAddonStartup.remove(true); + info(`**** restart AOM and rebuild XPI database`); + + await promiseStartupManager(); + + addons = await AddonManager.getAddonsByTypes(["extension"]); + Assert.equal(addons.length, 3, "addons installed"); + + let [a1, a2, a3] = await promiseAddonsByIDs([ + getID(`legacy-profile`), + getID(`sideload-profile`), + getID(`sideload-profile-2`), + ]); + + Assert.notEqual(a1, null); + Assert.ok(isExtensionInBootstrappedList(profileDir, a1.id)); + + Assert.notEqual(a2, null); + Assert.ok(isExtensionInBootstrappedList(profileDir, a2.id)); + + Assert.notEqual(a3, null); + Assert.ok(isExtensionInBootstrappedList(profileDir, a3.id)); + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_inject.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_inject.js new file mode 100644 index 0000000000..10bf7402d6 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_inject.js @@ -0,0 +1,429 @@ +// Enable signature checks for these tests +gUseRealCertChecks = true; +// Disable update security +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); + +const DATA = "data/signing_checks/"; +const ADDONS = { + bootstrap: { + unsigned: "unsigned_bootstrap_2.xpi", + badid: "signed_bootstrap_badid_2.xpi", + signed: "signed_bootstrap_2.xpi", + preliminary: "preliminary_bootstrap_2.xpi", + }, + nonbootstrap: { + unsigned: "unsigned_nonbootstrap_2.xpi", + badid: "signed_nonbootstrap_badid_2.xpi", + signed: "signed_nonbootstrap_2.xpi", + }, +}; +const ID = "test@tests.mozilla.org"; + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +// Deletes a file from the test add-on in the profile +function breakAddon(file) { + if (TEST_UNPACKED) { + let f = file.clone(); + f.append("test.txt"); + f.remove(true); + + f = file.clone(); + f.append("install.rdf"); + f.lastModifiedTime = Date.now(); + } else { + var zipW = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter); + zipW.open(file, FileUtils.MODE_RDWR | FileUtils.MODE_APPEND); + zipW.removeEntry("test.txt", false); + zipW.close(); + } +} + +function resetPrefs() { + Services.prefs.setIntPref("bootstraptest.active_version", -1); + Services.prefs.setIntPref("bootstraptest.installed_version", -1); + Services.prefs.setIntPref("bootstraptest.startup_reason", -1); + Services.prefs.setIntPref("bootstraptest.shutdown_reason", -1); + Services.prefs.setIntPref("bootstraptest.install_reason", -1); + Services.prefs.setIntPref("bootstraptest.uninstall_reason", -1); + Services.prefs.setIntPref("bootstraptest.startup_oldversion", -1); + Services.prefs.setIntPref("bootstraptest.shutdown_newversion", -1); + Services.prefs.setIntPref("bootstraptest.install_oldversion", -1); + Services.prefs.setIntPref("bootstraptest.uninstall_newversion", -1); +} + +function clearCache(file) { + if (TEST_UNPACKED) { + return; + } + + Services.obs.notifyObservers(file, "flush-cache-entry"); +} + +function getActiveVersion() { + return Services.prefs.getIntPref("bootstraptest.active_version"); +} + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4"); + + // Start and stop the manager to initialise everything in the profile before + // actual testing + await promiseStartupManager(); + await promiseShutdownManager(); + resetPrefs(); +}); + +// Injecting into profile (bootstrap) +add_task(async function () { + let file = await manuallyInstall( + do_get_file(DATA + ADDONS.bootstrap.unsigned), + profileDir, + ID + ); + + await promiseStartupManager(); + + // Currently we leave the sideloaded add-on there but just don't run it + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(addon.appDisabled); + Assert.ok(!addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_MISSING); + Assert.equal(getActiveVersion(), -1); + + await addon.uninstall(); + await promiseShutdownManager(); + resetPrefs(); + + Assert.ok(!file.exists()); + clearCache(file); +}); + +add_task(async function () { + let file = await manuallyInstall( + do_get_file(DATA + ADDONS.bootstrap.signed), + profileDir, + ID + ); + breakAddon(file); + + await promiseStartupManager(); + + // Currently we leave the sideloaded add-on there but just don't run it + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(addon.appDisabled); + Assert.ok(!addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN); + Assert.equal(getActiveVersion(), -1); + + await addon.uninstall(); + await promiseShutdownManager(); + resetPrefs(); + + Assert.ok(!file.exists()); + clearCache(file); +}); + +add_task(async function () { + let file = await manuallyInstall( + do_get_file(DATA + ADDONS.bootstrap.badid), + profileDir, + ID + ); + + await promiseStartupManager(); + + // Currently we leave the sideloaded add-on there but just don't run it + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(addon.appDisabled); + Assert.ok(!addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN); + Assert.equal(getActiveVersion(), -1); + + await addon.uninstall(); + await promiseShutdownManager(); + resetPrefs(); + + Assert.ok(!file.exists()); + clearCache(file); +}); + +// Installs a signed add-on then modifies it in place breaking its signing +add_task(async function () { + let file = await manuallyInstall( + do_get_file(DATA + ADDONS.bootstrap.signed), + profileDir, + ID + ); + + // Make it appear to come from the past so when we modify it later it is + // detected during startup. Obviously malware can bypass this method of + // detection but the periodic scan will catch that + await promiseSetExtensionModifiedTime(file.path, Date.now() - 600000); + + await promiseStartupManager(); + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_SIGNED); + Assert.equal(getActiveVersion(), 2); + + await promiseShutdownManager(); + Assert.equal(getActiveVersion(), 0); + + clearCache(file); + breakAddon(file); + resetPrefs(); + + await promiseStartupManager(); + + addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(addon.appDisabled); + Assert.ok(!addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN); + Assert.equal(getActiveVersion(), -1); + + let ids = AddonManager.getStartupChanges( + AddonManager.STARTUP_CHANGE_DISABLED + ); + Assert.equal(ids.length, 1); + Assert.equal(ids[0], ID); + + await addon.uninstall(); + await promiseShutdownManager(); + resetPrefs(); + + Assert.ok(!file.exists()); + clearCache(file); +}); + +// Injecting into profile (non-bootstrap) +add_task(async function () { + let file = await manuallyInstall( + do_get_file(DATA + ADDONS.nonbootstrap.unsigned), + profileDir, + ID + ); + + await promiseStartupManager(); + + // Currently we leave the sideloaded add-on there but just don't run it + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(addon.appDisabled); + Assert.ok(!addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_MISSING); + + await addon.uninstall(); + await promiseRestartManager(); + await promiseShutdownManager(); + + Assert.ok(!file.exists()); + clearCache(file); +}); + +add_task(async function () { + let file = await manuallyInstall( + do_get_file(DATA + ADDONS.nonbootstrap.signed), + profileDir, + ID + ); + breakAddon(file); + + await promiseStartupManager(); + + // Currently we leave the sideloaded add-on there but just don't run it + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(addon.appDisabled); + Assert.ok(!addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN); + + await addon.uninstall(); + await promiseRestartManager(); + await promiseShutdownManager(); + + Assert.ok(!file.exists()); + clearCache(file); +}); + +add_task(async function () { + let file = await manuallyInstall( + do_get_file(DATA + ADDONS.nonbootstrap.badid), + profileDir, + ID + ); + + await promiseStartupManager(); + + // Currently we leave the sideloaded add-on there but just don't run it + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(addon.appDisabled); + Assert.ok(!addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN); + + await addon.uninstall(); + await promiseRestartManager(); + await promiseShutdownManager(); + + Assert.ok(!file.exists()); + clearCache(file); +}); + +// Installs a signed add-on then modifies it in place breaking its signing +add_task(async function () { + let file = await manuallyInstall( + do_get_file(DATA + ADDONS.nonbootstrap.signed), + profileDir, + ID + ); + + // Make it appear to come from the past so when we modify it later it is + // detected during startup. Obviously malware can bypass this method of + // detection but the periodic scan will catch that + await promiseSetExtensionModifiedTime(file.path, Date.now() - 60000); + + await promiseStartupManager(); + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_SIGNED); + + await promiseShutdownManager(); + + clearCache(file); + breakAddon(file); + + await promiseStartupManager(); + + addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(addon.appDisabled); + Assert.ok(!addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN); + + let ids = AddonManager.getStartupChanges( + AddonManager.STARTUP_CHANGE_DISABLED + ); + Assert.equal(ids.length, 1); + Assert.equal(ids[0], ID); + + await addon.uninstall(); + await promiseRestartManager(); + await promiseShutdownManager(); + + Assert.ok(!file.exists()); + clearCache(file); +}); + +// Stage install then modify before startup (non-bootstrap) +add_task(async function () { + await promiseStartupManager(); + await promiseInstallAllFiles([ + do_get_file(DATA + ADDONS.nonbootstrap.signed), + ]); + await promiseShutdownManager(); + + let staged = profileDir.clone(); + staged.append("staged"); + staged.append(do_get_expected_addon_name(ID)); + Assert.ok(staged.exists()); + + breakAddon(staged); + await promiseStartupManager(); + + // Should have refused to install the broken staged version + let addon = await promiseAddonByID(ID); + Assert.equal(addon, null); + + clearCache(staged); + + await promiseShutdownManager(); +}); + +// Manufacture staged install (bootstrap) +add_task(async function () { + let stage = profileDir.clone(); + stage.append("staged"); + + let file = await manuallyInstall( + do_get_file(DATA + ADDONS.bootstrap.signed), + stage, + ID + ); + breakAddon(file); + + await promiseStartupManager(); + + // Should have refused to install the broken staged version + let addon = await promiseAddonByID(ID); + Assert.equal(addon, null); + Assert.equal(getActiveVersion(), -1); + + Assert.ok(!file.exists()); + clearCache(file); + + await promiseShutdownManager(); + resetPrefs(); +}); + +// Preliminarily-signed sideloaded add-ons should work +add_task(async function () { + let file = await manuallyInstall( + do_get_file(DATA + ADDONS.bootstrap.preliminary), + profileDir, + ID + ); + + await promiseStartupManager(); + + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_PRELIMINARY); + Assert.equal(getActiveVersion(), 2); + + await addon.uninstall(); + await promiseShutdownManager(); + resetPrefs(); + + Assert.ok(!file.exists()); + clearCache(file); +}); + +// Preliminarily-signed sideloaded add-ons should work via staged install +add_task(async function () { + let stage = profileDir.clone(); + stage.append("staged"); + + let file = await manuallyInstall( + do_get_file(DATA + ADDONS.bootstrap.preliminary), + stage, + ID + ); + + await promiseStartupManager(); + + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_PRELIMINARY); + Assert.equal(getActiveVersion(), 2); + + await addon.uninstall(); + await promiseShutdownManager(); + resetPrefs(); + + Assert.ok(!file.exists()); + clearCache(file); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_install.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_install.js new file mode 100644 index 0000000000..065463864d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_install.js @@ -0,0 +1,337 @@ +// Enable signature checks for these tests +gUseRealCertChecks = true; + +const DATA = "data/signing_checks/"; +const ADDONS = { + unsigned: "unsigned.xpi", + signed1: "signed1.xpi", + signed2: "signed2.xpi", + privileged: "privileged.xpi", + + // Bug 1509093 + // sha256Signed: "signed_bootstrap_sha256_1.xpi", +}; + +// The ID in signed1.xpi and signed2.xpi +const ID = "test@somewhere.com"; +const PR_USEC_PER_MSEC = 1000; + +let testserver = createHttpServer({ hosts: ["example.com"] }); + +Services.prefs.setCharPref( + "extensions.update.background.url", + "http://example.com/update.json" +); +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); + +// Creates an add-on with a broken signature by changing an existing file +function createBrokenAddonModify(file) { + let brokenFile = gTmpD.clone(); + brokenFile.append("broken.xpi"); + file.copyTo(brokenFile.parent, brokenFile.leafName); + + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.setData("FOOBAR", -1); + var zipW = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter); + zipW.open(brokenFile, FileUtils.MODE_RDWR | FileUtils.MODE_APPEND); + zipW.removeEntry("test.txt", false); + zipW.addEntryStream( + "test.txt", + new Date() * PR_USEC_PER_MSEC, + Ci.nsIZipWriter.COMPRESSION_NONE, + stream, + false + ); + zipW.close(); + + return brokenFile; +} + +// Creates an add-on with a broken signature by adding a new file +function createBrokenAddonAdd(file) { + let brokenFile = gTmpD.clone(); + brokenFile.append("broken.xpi"); + file.copyTo(brokenFile.parent, brokenFile.leafName); + + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.setData("FOOBAR", -1); + var zipW = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter); + zipW.open(brokenFile, FileUtils.MODE_RDWR | FileUtils.MODE_APPEND); + zipW.addEntryStream( + "test2.txt", + new Date() * PR_USEC_PER_MSEC, + Ci.nsIZipWriter.COMPRESSION_NONE, + stream, + false + ); + zipW.close(); + + return brokenFile; +} + +// Creates an add-on with a broken signature by removing an existing file +function createBrokenAddonRemove(file) { + let brokenFile = gTmpD.clone(); + brokenFile.append("broken.xpi"); + file.copyTo(brokenFile.parent, brokenFile.leafName); + + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.setData("FOOBAR", -1); + var zipW = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter); + zipW.open(brokenFile, FileUtils.MODE_RDWR | FileUtils.MODE_APPEND); + zipW.removeEntry("test.txt", false); + zipW.close(); + + return brokenFile; +} + +function serveUpdate(filename) { + const RESPONSE = { + addons: { + [ID]: { + updates: [ + { + version: "2.0", + update_link: `http://example.com/${filename}`, + applications: { + gecko: { + strict_min_version: "4", + advisory_max_version: "6", + }, + }, + }, + ], + }, + }, + }; + AddonTestUtils.registerJSON(testserver, "/update.json", RESPONSE); +} + +async function test_install_broken( + file, + expectedError, + expectNullAddon = true +) { + let install = await AddonManager.getInstallForFile(file); + await Assert.rejects( + install.install(), + /Install failed/, + "Install of an improperly signed extension should throw" + ); + + Assert.equal(install.state, AddonManager.STATE_DOWNLOAD_FAILED); + Assert.equal(install.error, expectedError); + + if (expectNullAddon) { + Assert.equal(install.addon, null); + } +} + +async function test_install_working(file, expectedSignedState) { + let install = await AddonManager.getInstallForFile(file); + await install.install(); + + Assert.equal(install.state, AddonManager.STATE_INSTALLED); + Assert.notEqual(install.addon, null); + Assert.equal(install.addon.signedState, expectedSignedState); + + await install.addon.uninstall(); +} + +async function test_update_broken(file1, file2, expectedError) { + // First install the older version + await Promise.all([ + promiseInstallFile(file1), + promiseWebExtensionStartup(ID), + ]); + + testserver.registerFile("/" + file2.leafName, file2); + serveUpdate(file2.leafName); + + let addon = await promiseAddonByID(ID); + let update = await promiseFindAddonUpdates(addon); + let install = update.updateAvailable; + await Assert.rejects( + install.install(), + /Install failed/, + "Update to an improperly signed extension should throw" + ); + + Assert.equal(install.state, AddonManager.STATE_DOWNLOAD_FAILED); + Assert.equal(install.error, expectedError); + Assert.equal(install.addon, null); + + testserver.registerFile("/" + file2.leafName, null); + testserver.registerPathHandler("/update.json", null); + + await addon.uninstall(); +} + +async function test_update_working(file1, file2, expectedSignedState) { + // First install the older version + await promiseInstallFile(file1); + + testserver.registerFile("/" + file2.leafName, file2); + serveUpdate(file2.leafName); + + let addon = await promiseAddonByID(ID); + let update = await promiseFindAddonUpdates(addon); + let install = update.updateAvailable; + await Promise.all([install.install(), promiseWebExtensionStartup(ID)]); + + Assert.equal(install.state, AddonManager.STATE_INSTALLED); + Assert.notEqual(install.addon, null); + Assert.equal(install.addon.signedState, expectedSignedState); + + testserver.registerFile("/" + file2.leafName, null); + testserver.registerPathHandler("/update.json", null); + + await install.addon.uninstall(); +} + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4"); + await promiseStartupManager(); +}); + +// Try to install a broken add-on +add_task(async function test_install_invalid_modified() { + let file = createBrokenAddonModify(do_get_file(DATA + ADDONS.signed1)); + await test_install_broken(file, AddonManager.ERROR_CORRUPT_FILE); + file.remove(true); +}); + +add_task(async function test_install_invalid_added() { + let file = createBrokenAddonAdd(do_get_file(DATA + ADDONS.signed1)); + await test_install_broken(file, AddonManager.ERROR_CORRUPT_FILE); + file.remove(true); +}); + +add_task(async function test_install_invalid_removed() { + let file = createBrokenAddonRemove(do_get_file(DATA + ADDONS.signed1)); + await test_install_broken(file, AddonManager.ERROR_CORRUPT_FILE); + file.remove(true); +}); + +// Try to install an unsigned add-on +add_task(async function test_install_invalid_unsigned() { + let file = do_get_file(DATA + ADDONS.unsigned); + await test_install_broken(file, AddonManager.ERROR_SIGNEDSTATE_REQUIRED); +}); + +// Try to install a signed add-on +add_task(async function test_install_valid() { + let file = do_get_file(DATA + ADDONS.signed1); + await test_install_working(file, AddonManager.SIGNEDSTATE_SIGNED); +}); + +add_task( + { + pref_set: [["xpinstall.signatures.dev-root", true]], + // `xpinstall.signatures.dev-root` is not taken into account on release + // builds because `MOZ_REQUIRE_SIGNING` is set to `true`. + skip_if: () => AppConstants.MOZ_REQUIRE_SIGNING, + }, + async function test_install_valid_file_with_different_root_cert() { + const TEST_CASES = [ + { + title: "XPI without ID in manifest", + xpi: "data/webext-implicit-id.xpi", + expectedMessage: + /Cannot find id for addon .+ Preference xpinstall.signatures.dev-root is set/, + }, + { + title: "XPI with ID in manifest", + xpi: DATA + ADDONS.signed1, + expectedMessage: /Add-on test@somewhere.com is not correctly signed/, + }, + ]; + + for (const { title, xpi, expectedMessage } of TEST_CASES) { + info(`test_install_valid_file_with_different_root_cert: ${title}`); + + const file = do_get_file(xpi); + + const awaitConsole = new Promise(resolve => { + Services.console.registerListener(function listener(message) { + if (expectedMessage.test(message.message)) { + Services.console.unregisterListener(listener); + resolve(); + } + }); + }); + + await test_install_broken( + file, + AddonManager.ERROR_CORRUPT_FILE, + // We don't expect the `addon` property on the `install` object to be + // `null` because that seems to happen later (when the signature is + // checked). + false + ); + + await awaitConsole; + } + } +); + +// Try to install an add-on signed with SHA-256 +add_task(async function test_install_valid_sha256() { + // Bug 1509093 + // let file = do_get_file(DATA + ADDONS.sha256Signed); + // await test_install_working(file, AddonManager.SIGNEDSTATE_SIGNED); +}); + +// Try to install an add-on with the "Mozilla Extensions" OU +add_task(async function test_install_valid_privileged() { + let file = do_get_file(DATA + ADDONS.privileged); + await test_install_working(file, AddonManager.SIGNEDSTATE_PRIVILEGED); +}); + +// Try to update to a broken add-on +add_task(async function test_update_invalid_modified() { + let file1 = do_get_file(DATA + ADDONS.signed1); + let file2 = createBrokenAddonModify(do_get_file(DATA + ADDONS.signed2)); + await test_update_broken(file1, file2, AddonManager.ERROR_CORRUPT_FILE); + file2.remove(true); +}); + +add_task(async function test_update_invalid_added() { + let file1 = do_get_file(DATA + ADDONS.signed1); + let file2 = createBrokenAddonAdd(do_get_file(DATA + ADDONS.signed2)); + await test_update_broken(file1, file2, AddonManager.ERROR_CORRUPT_FILE); + file2.remove(true); +}); + +add_task(async function test_update_invalid_removed() { + let file1 = do_get_file(DATA + ADDONS.signed1); + let file2 = createBrokenAddonRemove(do_get_file(DATA + ADDONS.signed2)); + await test_update_broken(file1, file2, AddonManager.ERROR_CORRUPT_FILE); + file2.remove(true); +}); + +// Try to update to an unsigned add-on +add_task(async function test_update_invalid_unsigned() { + let file1 = do_get_file(DATA + ADDONS.signed1); + let file2 = do_get_file(DATA + ADDONS.unsigned); + await test_update_broken( + file1, + file2, + AddonManager.ERROR_SIGNEDSTATE_REQUIRED + ); +}); + +// Try to update to a signed add-on +add_task(async function test_update_valid() { + let file1 = do_get_file(DATA + ADDONS.signed1); + let file2 = do_get_file(DATA + ADDONS.signed2); + await test_update_working(file1, file2, AddonManager.SIGNEDSTATE_SIGNED); +}); + +add_task(() => promiseShutdownManager()); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_langpack.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_langpack.js new file mode 100644 index 0000000000..8ad83b2ecb --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_langpack.js @@ -0,0 +1,67 @@ +const PREF_SIGNATURES_GENERAL = "xpinstall.signatures.required"; +const PREF_SIGNATURES_LANGPACKS = "extensions.langpacks.signatures.required"; + +// Disable "xpc::IsInAutomation()", since it would override the behavior +// we're testing for. +Services.prefs.setBoolPref( + "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer", + false +); + +// Try to install the given XPI file, and assert that the install +// succeeds. Uninstalls before returning. +async function installShouldSucceed(file) { + let install = await promiseInstallFile(file); + Assert.equal(install.state, AddonManager.STATE_INSTALLED); + Assert.notEqual(install.addon, null); + await install.addon.uninstall(); +} + +// Try to install the given XPI file, assert that the install fails +// due to lack of signing. +async function installShouldFail(file) { + let install; + try { + install = await AddonManager.getInstallForFile(file); + } catch (err) {} + Assert.equal(install.state, AddonManager.STATE_DOWNLOAD_FAILED); + Assert.equal(install.error, AddonManager.ERROR_SIGNEDSTATE_REQUIRED); + Assert.equal(install.addon, null); +} + +// Test that the preference controlling langpack signing works properly +// (and that the general preference for addon signing does not affect +// language packs). +add_task(async function () { + AddonTestUtils.useRealCertChecks = true; + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9"); + await promiseStartupManager(); + + Services.prefs.setBoolPref(PREF_SIGNATURES_GENERAL, true); + Services.prefs.setBoolPref(PREF_SIGNATURES_LANGPACKS, true); + + // The signed langpack should always install. + let signedXPI = do_get_file("data/signing_checks/langpack_signed.xpi"); + await installShouldSucceed(signedXPI); + + // With signatures required, unsigned langpack should not install. + let unsignedXPI = do_get_file("data/signing_checks/langpack_unsigned.xpi"); + await installShouldFail(unsignedXPI); + + // Even with the general xpi signing pref off, an unsigned langapck + // should not install. + Services.prefs.setBoolPref(PREF_SIGNATURES_GENERAL, false); + await installShouldFail(unsignedXPI); + + // But with the langpack signing pref off, unsigned langpack should + // install only on non-release builds. + Services.prefs.setBoolPref(PREF_SIGNATURES_LANGPACKS, false); + if (AppConstants.MOZ_REQUIRE_SIGNING) { + await installShouldFail(unsignedXPI); + } else { + await installShouldSucceed(unsignedXPI); + } + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_long.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_long.js new file mode 100644 index 0000000000..2aa76e8ff8 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_long.js @@ -0,0 +1,23 @@ +gUseRealCertChecks = true; + +const ID = "123456789012345678901234567890123456789012345678901@somewhere.com"; + +// Tests that signature verification works correctly on an extension with +// an ID that does not fit into a certificate CN field. +add_task(async function test_long_id() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + await promiseStartupManager(); + + Assert.greater(ID.length, 64, "ID is > 64 characters"); + + await promiseInstallFile(do_get_file("data/signing_checks/long.xpi")); + let addon = await promiseAddonByID(ID); + + Assert.notEqual(addon, null, "Addon install properly"); + Assert.ok( + addon.signedState > AddonManager.SIGNEDSTATE_MISSING, + "Signature verification worked properly" + ); + + await addon.uninstall(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_updatepref.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_updatepref.js new file mode 100644 index 0000000000..39abab438d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_updatepref.js @@ -0,0 +1,130 @@ +// Disable update security +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); +gUseRealCertChecks = true; + +const DATA = "data/signing_checks/"; +const ID = "test@somewhere.com"; + +let testserver = createHttpServer({ hosts: ["example.com"] }); + +AddonTestUtils.registerJSON(testserver, "/update.json", { + addons: { + [ID]: { + version: "2.0", + browser_specific_settings: { + gecko: { + strict_min_version: "4", + strict_max_version: "6", + }, + }, + }, + }, +}); + +Services.prefs.setCharPref( + "extensions.update.background.url", + "http://example.com/update.json" +); + +function verifySignatures() { + return new Promise(resolve => { + let observer = (subject, topic, data) => { + Services.obs.removeObserver(observer, "xpi-signature-changed"); + resolve(JSON.parse(data)); + }; + Services.obs.addObserver(observer, "xpi-signature-changed"); + + info("Verifying signatures"); + const { XPIExports } = ChromeUtils.importESModule( + "resource://gre/modules/addons/XPIExports.sys.mjs" + ); + XPIExports.XPIDatabase.verifySignatures(); + }); +} + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4"); +}); + +// Updating the pref without changing the app version won't disable add-ons +// immediately but will after a signing check +add_task(async function () { + Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, false); + await promiseStartupManager(); + + // Install an unsigned add-on + await promiseInstallFile(do_get_file(DATA + "unsigned.xpi")); + + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_MISSING); + + await promiseShutdownManager(); + + Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true); + + await promiseStartupManager(); + + addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_MISSING); + + // Update checks shouldn't affect the add-on + await AddonManagerPrivate.backgroundUpdateCheck(); + addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_MISSING); + + let changes = await verifySignatures(); + + Assert.equal(changes.disabled.length, 1); + Assert.equal(changes.disabled[0], ID); + + addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(addon.appDisabled); + Assert.ok(!addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_MISSING); + + await addon.uninstall(); + + await promiseShutdownManager(); +}); + +// Updating the pref with changing the app version will disable add-ons +// immediately +add_task(async function () { + Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, false); + await promiseStartupManager(); + + // Install an unsigned add-on + await promiseInstallFile(do_get_file(DATA + "unsigned.xpi")); + + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_MISSING); + + await promiseShutdownManager(); + + Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true); + gAppInfo.version = 5.0; + await promiseStartupManager(); + + addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(addon.appDisabled); + Assert.ok(!addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_MISSING); + + await addon.uninstall(); + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js new file mode 100644 index 0000000000..c17cb941cb --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js @@ -0,0 +1,109 @@ +// Enable signature checks for these tests +gUseRealCertChecks = true; + +const DATA = "data/signing_checks"; +const ID = "test@somewhere.com"; + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +function verifySignatures() { + return new Promise(resolve => { + let observer = (subject, topic, data) => { + Services.obs.removeObserver(observer, "xpi-signature-changed"); + resolve(JSON.parse(data)); + }; + Services.obs.addObserver(observer, "xpi-signature-changed"); + + info("Verifying signatures"); + const { XPIExports } = ChromeUtils.importESModule( + "resource://gre/modules/addons/XPIExports.sys.mjs" + ); + XPIExports.XPIDatabase.verifySignatures(); + }); +} + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4"); + +add_task(async function test_no_change() { + await promiseStartupManager(); + + // Install the first add-on + await promiseInstallFile(do_get_file(`${DATA}/signed1.xpi`)); + + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.equal(addon.appDisabled, false); + Assert.equal(addon.isActive, true); + Assert.equal(addon.pendingOperations, AddonManager.PENDING_NONE); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_SIGNED); + + // Swap in the files from the next add-on + manuallyUninstall(profileDir, ID); + await manuallyInstall(do_get_file(`${DATA}/signed2.xpi`), profileDir, ID); + + let listener = { + onPropetyChanged(_addon, properties) { + Assert.ok(false, `Got unexpected onPropertyChanged for ${_addon.id}`); + }, + }; + + AddonManager.addAddonListener(listener); + + // Trigger the check + let changes = await verifySignatures(); + Assert.equal(changes.enabled.length, 0); + Assert.equal(changes.disabled.length, 0); + + Assert.equal(addon.appDisabled, false); + Assert.equal(addon.isActive, true); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_SIGNED); + + await addon.uninstall(); + AddonManager.removeAddonListener(listener); +}); + +add_task(async function test_diable() { + // Install the first add-on + await promiseInstallFile(do_get_file(`${DATA}/signed1.xpi`)); + + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.ok(addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_SIGNED); + + // Swap in the files from the next add-on + manuallyUninstall(profileDir, ID); + await manuallyInstall(do_get_file(`${DATA}/unsigned.xpi`), profileDir, ID); + + let changedProperties = []; + let listener = { + onPropertyChanged(_, properties) { + changedProperties.push(...properties); + }, + }; + AddonManager.addAddonListener(listener); + + // Trigger the check + let [changes] = await Promise.all([ + verifySignatures(), + promiseAddonEvent("onDisabling"), + ]); + + Assert.equal(changes.enabled.length, 0); + Assert.equal(changes.disabled.length, 1); + Assert.equal(changes.disabled[0], ID); + + Assert.deepEqual( + changedProperties, + ["signedState", "appDisabled"], + "Got onPropertyChanged events for signedState and appDisabled" + ); + + Assert.ok(addon.appDisabled); + Assert.ok(!addon.isActive); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_MISSING); + + await addon.uninstall(); + AddonManager.removeAddonListener(listener); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_sitePermsAddonProvider.js b/toolkit/mozapps/extensions/test/xpcshell/test_sitePermsAddonProvider.js new file mode 100644 index 0000000000..51272509da --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_sitePermsAddonProvider.js @@ -0,0 +1,967 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + addGatedPermissionTypesForXpcShellTests, + SITEPERMS_ADDON_PROVIDER_PREF, + SITEPERMS_ADDON_BLOCKEDLIST_PREF, + SITEPERMS_ADDON_TYPE, +} = ChromeUtils.importESModule( + "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs" +); + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +const addonsBundle = new Localization(["toolkit/about/aboutAddons.ftl"], true); + +let ssm = Services.scriptSecurityManager; +const PRINCIPAL_COM = ssm.createContentPrincipalFromOrigin( + "https://example.com" +); +const PRINCIPAL_ORG = ssm.createContentPrincipalFromOrigin( + "https://example.org" +); +const PRINCIPAL_GITHUB = + ssm.createContentPrincipalFromOrigin("https://github.io"); +const PRINCIPAL_UNSECURE = + ssm.createContentPrincipalFromOrigin("http://example.net"); +const PRINCIPAL_IP = ssm.createContentPrincipalFromOrigin( + "https://18.154.122.194" +); +const PRINCIPAL_PRIVATEBROWSING = ssm.createContentPrincipal( + Services.io.newURI("https://example.withprivatebrowsing.com"), + { privateBrowsingId: 1 } +); +const PRINCIPAL_USERCONTEXT = ssm.createContentPrincipal( + Services.io.newURI("https://example.withusercontext.com"), + { userContextId: 2 } +); +const PRINCIPAL_NULL = ssm.createNullPrincipal({}); + +const URI_USED_IN_MULTIPLE_CONTEXTS = Services.io.newURI( + "https://multiplecontexts.com" +); +const PRINCIPAL_MULTIPLE_CONTEXTS_REGULAR = ssm.createContentPrincipal( + URI_USED_IN_MULTIPLE_CONTEXTS, + {} +); +const PRINCIPAL_MULTIPLE_CONTEXTS_PRIVATEBROWSING = ssm.createContentPrincipal( + URI_USED_IN_MULTIPLE_CONTEXTS, + { privateBrowsingId: 1 } +); +const PRINCIPAL_MULTIPLE_CONTEXTS_USERCONTEXT = ssm.createContentPrincipal( + URI_USED_IN_MULTIPLE_CONTEXTS, + { userContextId: 3 } +); + +const BLOCKED_DOMAIN = "malicious.com"; +const BLOCKED_DOMAIN2 = "someothermalicious.com"; +const BLOCKED_PRINCIPAL = ssm.createContentPrincipalFromOrigin( + `https://${BLOCKED_DOMAIN}` +); +const BLOCKED_PRINCIPAL2 = ssm.createContentPrincipalFromOrigin( + `https://${BLOCKED_DOMAIN2}` +); + +const GATED_SITE_PERM1 = "test/gatedSitePerm"; +const GATED_SITE_PERM2 = "test/anotherGatedSitePerm"; +addGatedPermissionTypesForXpcShellTests([GATED_SITE_PERM1, GATED_SITE_PERM2]); +const NON_GATED_SITE_PERM = "test/nonGatedPerm"; + +// Leave it to throw if the pref doesn't exist anymore, so that a test failure +// will make us notice and confirm if we have enabled it by default or not. +const PERMS_ISOLATE_USERCONTEXT_ENABLED = Services.prefs.getBoolPref( + "permissions.isolateBy.userContext" +); + +// Observers to bypass panels and continue install. +const expectAndHandleInstallPrompts = () => { + TestUtils.topicObserved("addon-install-blocked").then(([subject]) => { + let installInfo = subject.wrappedJSObject; + info("==== test got addon-install-blocked, calling `install`"); + installInfo.install(); + }); + TestUtils.topicObserved("webextension-permission-prompt").then( + ([subject]) => { + info("==== test webextension-permission-prompt, calling `resolve`"); + subject.wrappedJSObject.info.resolve(); + } + ); +}; + +add_setup(async () => { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + AddonTestUtils.init(this); + await promiseStartupManager(); +}); + +add_task( + { + pref_set: [[SITEPERMS_ADDON_PROVIDER_PREF, false]], + }, + async function test_sitepermsaddon_provider_disabled() { + // The SitePermsAddonProvider does not register until the first content process + // is launched, so we simulate that by firing this notification. + Services.obs.notifyObservers(null, "ipc:first-content-process-created"); + + ok( + !AddonManager.hasProvider("SitePermsAddonProvider"), + "Expect no SitePermsAddonProvider to be registered" + ); + } +); + +add_task( + { + pref_set: [ + [SITEPERMS_ADDON_PROVIDER_PREF, true], + [ + SITEPERMS_ADDON_BLOCKEDLIST_PREF, + `${BLOCKED_DOMAIN},${BLOCKED_DOMAIN2}`, + ], + ], + }, + async function test_sitepermsaddon_provider_enabled() { + // The SitePermsAddonProvider does not register until the first content process + // is launched, so we simulate that by firing this notification. + Services.obs.notifyObservers(null, "ipc:first-content-process-created"); + + ok( + AddonManager.hasProvider("SitePermsAddonProvider"), + "Expect SitePermsAddonProvider to be registered" + ); + + let addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal(addons.length, 0, "There's no addons"); + + info("Add a gated permission"); + PermissionTestUtils.add( + PRINCIPAL_COM, + GATED_SITE_PERM1, + Services.perms.ALLOW_ACTION + ); + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal(addons.length, 1, "A siteperm addon is now available"); + const comAddon = await promiseAddonByID(addons[0].id); + Assert.equal( + addons[0], + comAddon, + "getAddonByID returns the expected addon" + ); + + Assert.deepEqual( + comAddon.sitePermissions, + [GATED_SITE_PERM1], + "addon has expected sitePermissions" + ); + Assert.equal( + comAddon.type, + SITEPERMS_ADDON_TYPE, + "addon has expected type" + ); + + const localizedExtensionName = await addonsBundle.formatValue( + "addon-sitepermission-host", + { + host: PRINCIPAL_COM.host, + } + ); + Assert.ok(!!localizedExtensionName, "retrieved addonName is not falsy"); + Assert.equal( + comAddon.name, + localizedExtensionName, + "addon has expected name" + ); + + info("Add another gated permission"); + PermissionTestUtils.add( + PRINCIPAL_COM, + GATED_SITE_PERM2, + Services.perms.ALLOW_ACTION + ); + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal( + addons.length, + 1, + "There is no new siteperm addon after adding a permission to the same principal..." + ); + Assert.deepEqual( + comAddon.sitePermissions, + [GATED_SITE_PERM1, GATED_SITE_PERM2], + "...but the new permission is reported by addon.sitePermissions" + ); + + info("Add a non-gated permission"); + PermissionTestUtils.add( + PRINCIPAL_COM, + NON_GATED_SITE_PERM, + Services.perms.ALLOW_ACTION + ); + Assert.equal( + addons.length, + 1, + "There is no new siteperm addon after adding a non gated permission to the same principal..." + ); + Assert.deepEqual( + comAddon.sitePermissions, + [GATED_SITE_PERM1, GATED_SITE_PERM2], + "...and the new permission is not reported by addon.sitePermissions" + ); + + info("Adding a gated permission to another principal"); + PermissionTestUtils.add( + PRINCIPAL_ORG, + GATED_SITE_PERM1, + Services.perms.ALLOW_ACTION + ); + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal(addons.length, 2, "A new siteperm addon is now available"); + const orgAddon = await promiseAddonByID(addons[1].id); + Assert.equal( + addons[1], + orgAddon, + "getAddonByID returns the expected addon" + ); + + Assert.deepEqual( + orgAddon.sitePermissions, + [GATED_SITE_PERM1], + "new addon only has a single sitePermission" + ); + + info( + "Passing null or undefined to getAddonsByTypes returns all the addons" + ); + addons = await promiseAddonsByTypes(null); + // We can't do an exact check on all the returned addons as we get other type + // of addons from other providers. + Assert.deepEqual( + addons.filter(a => a.type == SITEPERMS_ADDON_TYPE).map(a => a.id), + [comAddon.id, orgAddon.id], + "Got site perms addons when passing null" + ); + + addons = await promiseAddonsByTypes(); + Assert.deepEqual( + addons.filter(a => a.type == SITEPERMS_ADDON_TYPE).map(a => a.id), + [comAddon.id, orgAddon.id], + "Got site perms addons when passing undefined" + ); + + info("Remove a gated permission"); + PermissionTestUtils.remove(PRINCIPAL_COM, GATED_SITE_PERM2); + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal( + addons.length, + 2, + "Removing a permission did not removed the addon has it has another permission" + ); + Assert.deepEqual( + comAddon.sitePermissions, + [GATED_SITE_PERM1], + "addon has expected sitePermissions" + ); + + info("Remove last gated permission on PRINCIPAL_COM"); + const promisePrincipalComUninstalling = AddonTestUtils.promiseAddonEvent( + "onUninstalling", + addon => { + return addon.id === comAddon.id; + } + ); + const promisePrincipalComUninstalled = AddonTestUtils.promiseAddonEvent( + "onUninstalled", + addon => { + return addon.id === comAddon.id; + } + ); + PermissionTestUtils.remove(PRINCIPAL_COM, GATED_SITE_PERM1); + info("Wait for onUninstalling addon listener call"); + await promisePrincipalComUninstalling; + info("Wait for onUninstalled addon listener call"); + await promisePrincipalComUninstalled; + + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal( + addons.length, + 1, + "Removing the last gated permission removed the addon" + ); + Assert.equal(addons[0], orgAddon); + + info("Uninstall org addon"); + orgAddon.uninstall(); + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal(addons.length, 0, "org addon is removed"); + Assert.equal( + PermissionTestUtils.testExactPermission(PRINCIPAL_ORG, GATED_SITE_PERM1), + false, + "Permission was removed when the addon was uninstalled" + ); + + info("Add gated permissions"); + PermissionTestUtils.add( + PRINCIPAL_COM, + GATED_SITE_PERM1, + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + PRINCIPAL_ORG, + GATED_SITE_PERM1, + Services.perms.ALLOW_ACTION + ); + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal(addons.length, 2, "2 addons are now available"); + + info("Clear permissions"); + const onAddon1Uninstall = AddonTestUtils.promiseAddonEvent( + "onUninstalled", + addon => addon.id === addons[0].id + ); + const onAddon2Uninstall = AddonTestUtils.promiseAddonEvent( + "onUninstalled", + addon => addon.id === addons[1].id + ); + Services.perms.removeAll(); + + await Promise.all([onAddon1Uninstall, onAddon2Uninstall]); + ok("Addons were properly uninstalled…"); + Assert.equal( + (await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE])).length, + 0, + "… and getAddonsByTypes does not return them anymore" + ); + + info("Adding a permission to a public etld"); + PermissionTestUtils.add( + PRINCIPAL_GITHUB, + GATED_SITE_PERM1, + Services.perms.ALLOW_ACTION + ); + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal( + addons.length, + 0, + "Adding a gated permission to a public etld shouldn't add a new addon" + ); + // Cleanup + PermissionTestUtils.remove(PRINCIPAL_GITHUB, GATED_SITE_PERM1); + + info("Adding a permission to a non secure principal"); + PermissionTestUtils.add( + PRINCIPAL_UNSECURE, + GATED_SITE_PERM1, + Services.perms.ALLOW_ACTION + ); + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal( + addons.length, + 0, + "Adding a gated permission to an unsecure principal shouldn't add a new addon" + ); + + info("Adding a permission to a blocked principal"); + PermissionTestUtils.add( + BLOCKED_PRINCIPAL, + GATED_SITE_PERM1, + Services.perms.ALLOW_ACTION + ); + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal( + addons.length, + 0, + "Adding a gated permission to a blocked principal shouldn't add a new addon" + ); + // Cleanup + PermissionTestUtils.remove(BLOCKED_PRINCIPAL, GATED_SITE_PERM1); + + info("Call installSitePermsAddonFromWebpage without proper principal"); + await Assert.rejects( + AddonManager.installSitePermsAddonFromWebpage( + null, + // principal + null, + GATED_SITE_PERM1 + ), + /aInstallingPrincipal must be a nsIPrincipal/, + "installSitePermsAddonFromWebpage rejected when called without a principal" + ); + + info( + "Call installSitePermsAddonFromWebpage with non-null, non-element browser" + ); + await Assert.rejects( + AddonManager.installSitePermsAddonFromWebpage( + "browser", + PRINCIPAL_COM, + GATED_SITE_PERM1 + ), + /aBrowser must be an Element, or null/, + "installSitePermsAddonFromWebpage rejected when called with a non-null, non-element browser" + ); + + info("Call installSitePermsAddonFromWebpage with unsecure principal"); + await Assert.rejects( + AddonManager.installSitePermsAddonFromWebpage( + null, + PRINCIPAL_UNSECURE, + GATED_SITE_PERM1 + ), + /SitePermsAddons can only be installed from secure origins/, + "installSitePermsAddonFromWebpage rejected when called with unsecure principal" + ); + + info( + "Call installSitePermsAddonFromWebpage for public principal and gated permission" + ); + await Assert.rejects( + AddonManager.installSitePermsAddonFromWebpage( + null, + PRINCIPAL_GITHUB, + GATED_SITE_PERM1 + ), + /SitePermsAddon can\'t be installed from public eTLDs/, + "installSitePermsAddonFromWebpage rejected when called with public principal" + ); + + info( + "Call installSitePermsAddonFromWebpage for a NullPrincipal as installingPrincipal" + ); + await Assert.rejects( + AddonManager.installSitePermsAddonFromWebpage( + null, + PRINCIPAL_NULL, + GATED_SITE_PERM1 + ), + /SitePermsAddons can\'t be installed from sandboxed subframes/, + "installSitePermsAddonFromWebpage rejected when called with a NullPrincipal as installing principal" + ); + + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal(addons.length, 0, "there was no added addon..."); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_GITHUB, + GATED_SITE_PERM1 + ), + false, + "...and no new permission either" + ); + + info( + "Call installSitePermsAddonFromWebpage for plain-ip principal and gated permission" + ); + await Assert.rejects( + AddonManager.installSitePermsAddonFromWebpage( + null, + PRINCIPAL_IP, + GATED_SITE_PERM1 + ), + /SitePermsAddons install disallowed when the host is an IP address/, + "installSitePermsAddonFromWebpage rejected when called with plain-ip principal" + ); + + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal(addons.length, 0, "there was no added addon..."); + Assert.equal( + PermissionTestUtils.testExactPermission(PRINCIPAL_IP, GATED_SITE_PERM1), + false, + "...and no new permission either" + ); + + info( + "Call installSitePermsAddonFromWebpage for authorized principal and non-gated permission" + ); + await Assert.rejects( + AddonManager.installSitePermsAddonFromWebpage( + null, + PRINCIPAL_COM, + NON_GATED_SITE_PERM + ), + new RegExp(`"${NON_GATED_SITE_PERM}" is not a gated permission`), + "installSitePermsAddonFromWebpage rejected for non-gated permission" + ); + + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal( + addons.length, + 0, + "installSitePermsAddonFromWebpage with non-gated permission should not add the addon" + ); + + info("Call installSitePermsAddonFromWebpage blocked principals"); + const blockedDomainsPrincipals = { + [BLOCKED_DOMAIN]: BLOCKED_PRINCIPAL, + [BLOCKED_DOMAIN2]: BLOCKED_PRINCIPAL2, + }; + for (const blockedDomain of [BLOCKED_DOMAIN, BLOCKED_DOMAIN2]) { + await Assert.rejects( + AddonManager.installSitePermsAddonFromWebpage( + null, + blockedDomainsPrincipals[blockedDomain], + GATED_SITE_PERM1 + ), + /SitePermsAddons can\'t be installed/, + `installSitePermsAddonFromWebpage rejected when called with blocked domain principal: ${blockedDomainsPrincipals[blockedDomain].URI.spec}` + ); + await Assert.rejects( + AddonManager.installSitePermsAddonFromWebpage( + null, + ssm.createContentPrincipalFromOrigin(`https://sub.${blockedDomain}`), + GATED_SITE_PERM1 + ), + /SitePermsAddons can\'t be installed/, + `installSitePermsAddonFromWebpage rejected when called with blocked subdomain principal: https://sub.${blockedDomain}` + ); + } + + info( + "Call installSitePermsAddonFromWebpage for authorized principal and gated permission" + ); + expectAndHandleInstallPrompts(); + const onAddonInstalled = AddonTestUtils.promiseInstallEvent( + "onInstallEnded" + ).then(installs => installs?.[0]?.addon); + + await AddonManager.installSitePermsAddonFromWebpage( + null, + PRINCIPAL_COM, + GATED_SITE_PERM1 + ); + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal( + addons.length, + 1, + "installSitePermsAddonFromWebpage should add the addon..." + ); + Assert.equal( + PermissionTestUtils.testExactPermission(PRINCIPAL_COM, GATED_SITE_PERM1), + true, + "...and set the permission" + ); + + // The addon we get here is a SitePermsAddonInstalling instance, and we want to assert + // that its permissions are correct as it may impact addon uninstall later on + Assert.deepEqual( + (await onAddonInstalled).sitePermissions, + [GATED_SITE_PERM1], + "Addon has expected sitePermissions" + ); + + info( + "Call installSitePermsAddonFromWebpage for private browsing principal and gated permission" + ); + expectAndHandleInstallPrompts(); + await AddonManager.installSitePermsAddonFromWebpage( + null, + PRINCIPAL_PRIVATEBROWSING, + GATED_SITE_PERM1 + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_PRIVATEBROWSING, + GATED_SITE_PERM1 + ), + true, + "...and set the permission" + ); + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal( + addons.length, + 2, + "installSitePermsAddonFromWebpage should add the addon..." + ); + const [addonWithPrivateBrowsing] = addons.filter( + addon => addon.siteOrigin === PRINCIPAL_PRIVATEBROWSING.siteOriginNoSuffix + ); + Assert.equal( + addonWithPrivateBrowsing?.siteOrigin, + PRINCIPAL_PRIVATEBROWSING.siteOriginNoSuffix, + "Got an addon for the expected siteOriginNoSuffix value" + ); + await addonWithPrivateBrowsing.uninstall(); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_PRIVATEBROWSING, + GATED_SITE_PERM1 + ), + false, + "Uninstalling the addon should clear the permission for the private browsing principal" + ); + + info( + "Call installSitePermsAddonFromWebpage for user context isolated principal and gated permission" + ); + expectAndHandleInstallPrompts(); + await AddonManager.installSitePermsAddonFromWebpage( + null, + PRINCIPAL_USERCONTEXT, + GATED_SITE_PERM1 + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_USERCONTEXT, + GATED_SITE_PERM1 + ), + true, + "...and set the permission" + ); + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal( + addons.length, + 2, + "installSitePermsAddonFromWebpage should add the addon..." + ); + const [addonWithUserContextId] = addons.filter( + addon => addon.siteOrigin === PRINCIPAL_USERCONTEXT.siteOriginNoSuffix + ); + Assert.equal( + addonWithUserContextId?.siteOrigin, + PRINCIPAL_USERCONTEXT.siteOriginNoSuffix, + "Got an addon for the expected siteOriginNoSuffix value" + ); + await addonWithUserContextId.uninstall(); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_USERCONTEXT, + GATED_SITE_PERM1 + ), + false, + "Uninstalling the addon should clear the permission for the user context isolated principal" + ); + + info( + "Check calling installSitePermsAddonFromWebpage for same gated permission and same origin on different contexts" + ); + info("First call installSitePermsAddonFromWebpage on regular context"); + expectAndHandleInstallPrompts(); + await AddonManager.installSitePermsAddonFromWebpage( + null, + PRINCIPAL_MULTIPLE_CONTEXTS_REGULAR, + GATED_SITE_PERM1 + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_MULTIPLE_CONTEXTS_REGULAR, + GATED_SITE_PERM1 + ), + true, + "...and set the permission" + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_MULTIPLE_CONTEXTS_USERCONTEXT, + GATED_SITE_PERM1 + ), + // We expect the permission to not be set for user context if + // perms.isolateBy.userContext is set to true. + PERMS_ISOLATE_USERCONTEXT_ENABLED + ? Services.perms.UNKNOWN_ACTION + : Services.perms.ALLOW_ACTION, + `...and ${ + PERMS_ISOLATE_USERCONTEXT_ENABLED ? "not allowed" : "allowed" + } on the context user specific principal` + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_MULTIPLE_CONTEXTS_PRIVATEBROWSING, + GATED_SITE_PERM1 + ), + false, + "...but not on the private browsing principal" + ); + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal( + addons.length, + 2, + "installSitePermsAddonFromWebpage should add the addon..." + ); + const [addonMultipleContexts] = addons.filter( + addon => + addon.siteOrigin === + PRINCIPAL_MULTIPLE_CONTEXTS_REGULAR.siteOriginNoSuffix + ); + Assert.equal( + addonMultipleContexts?.siteOrigin, + PRINCIPAL_MULTIPLE_CONTEXTS_REGULAR.siteOriginNoSuffix, + "Got an addon for the expected siteOriginNoSuffix value" + ); + info("Then call installSitePermsAddonFromWebpage on private context"); + expectAndHandleInstallPrompts(); + await AddonManager.installSitePermsAddonFromWebpage( + null, + PRINCIPAL_MULTIPLE_CONTEXTS_PRIVATEBROWSING, + GATED_SITE_PERM1 + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_MULTIPLE_CONTEXTS_PRIVATEBROWSING, + GATED_SITE_PERM1 + ), + true, + "Permission is set for the private context" + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_MULTIPLE_CONTEXTS_REGULAR, + GATED_SITE_PERM1 + ), + true, + "...and still set on the regular principal" + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_MULTIPLE_CONTEXTS_USERCONTEXT, + GATED_SITE_PERM1 + ), + // We expect the permission to not be set for user context if + // perms.isolateBy.userContext is set to true. + PERMS_ISOLATE_USERCONTEXT_ENABLED + ? Services.perms.UNKNOWN_ACTION + : Services.perms.ALLOW_ACTION, + `...and ${ + PERMS_ISOLATE_USERCONTEXT_ENABLED ? "not allowed" : "allowed" + } on the context user specific principal` + ); + + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal( + addons.length, + 2, + "installSitePermsAddonFromWebpage did not add a new addon" + ); + info("Then call installSitePermsAddonFromWebpage on specific user context"); + expectAndHandleInstallPrompts(); + await AddonManager.installSitePermsAddonFromWebpage( + null, + PRINCIPAL_MULTIPLE_CONTEXTS_USERCONTEXT, + GATED_SITE_PERM1 + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_MULTIPLE_CONTEXTS_USERCONTEXT, + GATED_SITE_PERM1 + ), + true, + "Permission is set for the user context" + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_MULTIPLE_CONTEXTS_REGULAR, + GATED_SITE_PERM1 + ), + true, + "...and still set on the regular principal" + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_MULTIPLE_CONTEXTS_PRIVATEBROWSING, + GATED_SITE_PERM1 + ), + true, + "...and on the private principal" + ); + + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal( + addons.length, + 2, + "installSitePermsAddonFromWebpage did not add a new addon" + ); + + info( + "Uninstalling the addon should remove the permission on the different contexts" + ); + await addonMultipleContexts.uninstall(); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_MULTIPLE_CONTEXTS_REGULAR, + GATED_SITE_PERM1 + ), + false, + "Uninstalling the addon should clear the permission for regular principal..." + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_MULTIPLE_CONTEXTS_PRIVATEBROWSING, + GATED_SITE_PERM1 + ), + false, + "... as well as the private browsing one..." + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_MULTIPLE_CONTEXTS_USERCONTEXT, + GATED_SITE_PERM1 + ), + false, + "... and the user context one" + ); + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal(addons.length, 1, "addon was properly uninstalled"); + + info("Install the addon for the multiple context origin again"); + expectAndHandleInstallPrompts(); + await AddonManager.installSitePermsAddonFromWebpage( + null, + PRINCIPAL_MULTIPLE_CONTEXTS_REGULAR, + GATED_SITE_PERM1 + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_MULTIPLE_CONTEXTS_REGULAR, + GATED_SITE_PERM1 + ), + true, + "...and set the permission" + ); + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal( + addons.length, + 2, + "installSitePermsAddonFromWebpage should add the addon..." + ); + + info("Then call installSitePermsAddonFromWebpage on private context"); + expectAndHandleInstallPrompts(); + await AddonManager.installSitePermsAddonFromWebpage( + null, + PRINCIPAL_MULTIPLE_CONTEXTS_PRIVATEBROWSING, + GATED_SITE_PERM1 + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_MULTIPLE_CONTEXTS_PRIVATEBROWSING, + GATED_SITE_PERM1 + ), + true, + "Permission is set for the private context" + ); + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal( + addons.length, + 2, + "installSitePermsAddonFromWebpage did not add a new addon" + ); + + info("Remove the permission for the private context"); + PermissionTestUtils.remove( + PRINCIPAL_MULTIPLE_CONTEXTS_PRIVATEBROWSING, + GATED_SITE_PERM1 + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_MULTIPLE_CONTEXTS_PRIVATEBROWSING, + GATED_SITE_PERM1 + ), + false, + "Permission is removed for the private context..." + ); + + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal(addons.length, 2, "... but it didn't uninstall the addon"); + + info("Remove the permission for the regular context"); + PermissionTestUtils.remove( + PRINCIPAL_MULTIPLE_CONTEXTS_REGULAR, + GATED_SITE_PERM1 + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + PRINCIPAL_MULTIPLE_CONTEXTS_REGULAR, + GATED_SITE_PERM1 + ), + false, + "Permission is removed for the regular context..." + ); + + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal(addons.length, 1, "...and addon is uninstalled"); + + await addons[0]?.uninstall(); + } +); + +add_task( + { + pref_set: [[SITEPERMS_ADDON_PROVIDER_PREF, true]], + }, + async function test_salted_hash_addon_id() { + // Make sure the test will also be able to run if it is the only one executed. + Services.obs.notifyObservers(null, "ipc:first-content-process-created"); + ok( + AddonManager.hasProvider("SitePermsAddonProvider"), + "Expect SitePermsAddonProvider to be registered" + ); + // Make sure no sitepermission addon is already installed. + let addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal(addons.length, 0, "There's no addons"); + + expectAndHandleInstallPrompts(); + await AddonManager.installSitePermsAddonFromWebpage( + null, + PRINCIPAL_COM, + GATED_SITE_PERM1 + ); + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal( + addons.length, + 1, + "installSitePermsAddonFromWebpage should add the addon..." + ); + Assert.equal( + PermissionTestUtils.testExactPermission(PRINCIPAL_COM, GATED_SITE_PERM1), + true, + "...and set the permission" + ); + + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal(addons.length, 1, "There is an addon installed"); + + const firstSaltedAddonId = addons[0].id; + ok(firstSaltedAddonId, "Got the first addon id"); + + info("Verify addon id after mocking new browsing session"); + + const { generateSalt } = ChromeUtils.importESModule( + "resource://gre/modules/addons/SitePermsAddonProvider.sys.mjs" + ); + generateSalt(); + + await promiseRestartManager(); + + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal(addons.length, 1, "There is an addon installed"); + + const secondSaltedAddonId = addons[0].id; + ok( + secondSaltedAddonId, + "Got the second addon id after mocking new browsing session" + ); + + Assert.notEqual( + firstSaltedAddonId, + secondSaltedAddonId, + "The two addon ids are different" + ); + + // Confirm that new installs from the same siteOrigin will still + // belong to the existing addon entry while the salt isn't expected + // to have changed. + expectAndHandleInstallPrompts(); + await AddonManager.installSitePermsAddonFromWebpage( + null, + PRINCIPAL_COM, + GATED_SITE_PERM1 + ); + + addons = await promiseAddonsByTypes([SITEPERMS_ADDON_TYPE]); + Assert.equal(addons.length, 1, "There is still a single addon installed"); + + await addons[0]?.uninstall(); + } +); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_startup.js b/toolkit/mozapps/extensions/test/xpcshell/test_startup.js new file mode 100644 index 0000000000..ba9e04c7bf --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_startup.js @@ -0,0 +1,648 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies startup detection of added/removed/changed items and install +// location priorities + +Services.prefs.setIntPref("extensions.autoDisableScopes", 0); + +const ID1 = getID(1); +const ID2 = getID(2); +const ID3 = getID(3); +const ID4 = getID(4); + +function createWebExtensionXPI(id, version) { + return createTempWebExtensionFile({ + manifest: { + version, + browser_specific_settings: { gecko: { id } }, + }, + }); +} + +// Bug 1554703: Verify that the extensions.lastAppBuildId pref is updated properly +// to avoid a full scan on second startup in XPIStates.scanForChanges. +add_task(async function test_scan_app_build_id_updated() { + const PREF_EM_LAST_APP_BUILD_ID = "extensions.lastAppBuildId"; + Assert.equal( + Services.prefs.getCharPref(PREF_EM_LAST_APP_BUILD_ID, ""), + "", + "fresh version with no saved build ID" + ); + Assert.ok(Services.appinfo.appBuildID, "build ID is set before a startup"); + + await promiseStartupManager(); + Assert.equal( + Services.prefs.getCharPref(PREF_EM_LAST_APP_BUILD_ID, ""), + Services.appinfo.appBuildID, + "build ID is correct after a startup" + ); + + await promiseShutdownManager(); +}); + +// Try to install all the items into the profile +add_task(async function test_scan_profile() { + await promiseStartupManager(); + + let ids = []; + for (let n of [1, 2, 3]) { + let id = getID(n); + ids.push(id); + await createWebExtension(id, initialVersion(n), profileDir); + } + + await promiseRestartManager(); + let addons = await AddonManager.getAddonsByTypes(["extension"]); + Assert.equal(addons.length, 3, "addons installed"); + + check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, ids); + check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []); + + info("Checking for " + gAddonStartup.path); + Assert.ok(gAddonStartup.exists()); + + for (let n of [1, 2, 3]) { + let id = getID(n); + let addon = await promiseAddonByID(id); + Assert.notEqual(addon, null); + Assert.equal(addon.id, id); + Assert.notEqual(addon.syncGUID, null); + Assert.ok(addon.syncGUID.length >= 9); + Assert.equal(addon.version, initialVersion(n)); + Assert.ok(isExtensionInBootstrappedList(profileDir, id)); + Assert.ok(hasFlag(addon.permissions, AddonManager.PERM_CAN_UNINSTALL)); + Assert.ok(hasFlag(addon.permissions, AddonManager.PERM_CAN_UPGRADE)); + do_check_in_crash_annotation(id, initialVersion(n)); + Assert.equal(addon.scope, AddonManager.SCOPE_PROFILE); + Assert.equal(addon.sourceURI, null); + Assert.ok(addon.foreignInstall); + Assert.ok(!addon.userDisabled); + Assert.ok(addon.seen); + } + + let extensionAddons = await AddonManager.getAddonsByTypes(["extension"]); + Assert.equal(extensionAddons.length, 3); + + await promiseShutdownManager(); +}); + +// Test that modified items are detected and items in other install locations +// are ignored +add_task(async function test_modify() { + await createWebExtension(ID1, "1.1", userDir); + await createWebExtension(ID2, "2.1", profileDir); + await createWebExtension(ID2, "2.2", globalDir); + await createWebExtension(ID2, "2.3", userDir); + + await IOUtils.remove(PathUtils.join(profileDir.path, `${ID3}.xpi`)); + + await promiseStartupManager(); + let addons = await AddonManager.getAddonsByTypes(["extension"]); + Assert.equal(addons.length, 2, "addons installed"); + + check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, [ID2]); + check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, [ID3]); + check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []); + + Assert.ok(gAddonStartup.exists()); + + let [a1, a2, a3] = await AddonManager.getAddonsByIDs([ID1, ID2, ID3]); + + Assert.notEqual(a1, null); + Assert.equal(a1.id, ID1); + Assert.equal(a1.version, "1.0"); + Assert.ok(isExtensionInBootstrappedList(profileDir, ID1)); + Assert.ok(!isExtensionInBootstrappedList(userDir, ID1)); + Assert.ok(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL)); + Assert.ok(hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE)); + do_check_in_crash_annotation(ID1, "1.0"); + Assert.equal(a1.scope, AddonManager.SCOPE_PROFILE); + Assert.ok(a1.foreignInstall); + + // The version in the profile should take precedence. + const VERSION2 = "2.1"; + Assert.notEqual(a2, null); + Assert.equal(a2.id, ID2); + Assert.equal(a2.version, VERSION2); + Assert.ok(isExtensionInBootstrappedList(profileDir, ID2)); + Assert.ok(!isExtensionInBootstrappedList(userDir, ID2)); + Assert.ok(!isExtensionInBootstrappedList(globalDir, ID2)); + Assert.ok(hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL)); + Assert.ok(hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE)); + do_check_in_crash_annotation(ID2, VERSION2); + Assert.equal(a2.scope, AddonManager.SCOPE_PROFILE); + Assert.ok(a2.foreignInstall); + + Assert.equal(a3, null); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID3)); + do_check_not_in_crash_annotation(ID3, "3.0"); + + await promiseShutdownManager(); +}); + +// Check that removing items from the profile reveals their hidden versions. +add_task(async function test_reveal() { + await IOUtils.remove(PathUtils.join(profileDir.path, `${ID1}.xpi`)); + await IOUtils.remove(PathUtils.join(profileDir.path, `${ID2}.xpi`)); + + // XPI with wrong name (basename doesn't match the id) + let xpi = await createWebExtensionXPI(ID3, "3.0"); + xpi.copyTo(profileDir, `${ID4}.xpi`); + + await promiseStartupManager(); + let addons = await AddonManager.getAddonsByTypes(["extension"]); + Assert.equal(addons.length, 2, "addons installed"); + + check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, [ID1, ID2]); + check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []); + + let [a1, a2, a3, a4] = await AddonManager.getAddonsByIDs([ + ID1, + ID2, + ID3, + ID4, + ]); + + // Copy of addon1 in the per-user directory is now revealed. + const VERSION1 = "1.1"; + Assert.notEqual(a1, null); + Assert.equal(a1.id, ID1); + Assert.equal(a1.version, VERSION1); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID1)); + Assert.ok(isExtensionInBootstrappedList(userDir, ID1)); + Assert.ok(!hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL)); + Assert.ok(!hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE)); + do_check_in_crash_annotation(ID1, VERSION1); + Assert.equal(a1.scope, AddonManager.SCOPE_USER); + + // Likewise with addon2 + const VERSION2 = "2.3"; + Assert.notEqual(a2, null); + Assert.equal(a2.id, ID2); + Assert.equal(a2.version, VERSION2); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID2)); + Assert.ok(isExtensionInBootstrappedList(userDir, ID2)); + Assert.ok(!isExtensionInBootstrappedList(globalDir, ID2)); + Assert.ok(!hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL)); + Assert.ok(!hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE)); + do_check_in_crash_annotation(ID2, VERSION2); + Assert.equal(a2.scope, AddonManager.SCOPE_USER); + + Assert.equal(a3, null); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID3)); + + Assert.equal(a4, null); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID4)); + + let addon4Exists = await IOUtils.exists( + PathUtils.join(profileDir.path, `${ID4}.xpi`) + ); + Assert.ok(!addon4Exists, "Misnamed xpi should be removed from profile"); + + await promiseShutdownManager(); +}); + +// Test that disabling an install location works +add_task(async function test_disable_location() { + Services.prefs.setIntPref( + "extensions.enabledScopes", + AddonManager.SCOPE_SYSTEM + ); + + await promiseStartupManager(); + let addons = await AddonManager.getAddonsByTypes(["extension"]); + Assert.equal(addons.length, 1, "addons installed"); + + check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, [ID2]); + check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, [ID1]); + check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []); + + let [a1, a2] = await AddonManager.getAddonsByIDs([ID1, ID2]); + Assert.equal(a1, null); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID1)); + Assert.ok(!isExtensionInBootstrappedList(userDir, ID1)); + + // System-wide copy of addon2 is now revealed + const VERSION2 = "2.2"; + Assert.notEqual(a2, null); + Assert.equal(a2.id, ID2); + Assert.equal(a2.version, VERSION2); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID2)); + Assert.ok(!isExtensionInBootstrappedList(userDir, ID2)); + Assert.ok(isExtensionInBootstrappedList(globalDir, ID2)); + Assert.ok(!hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL)); + Assert.ok(!hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE)); + do_check_in_crash_annotation(ID2, VERSION2); + Assert.equal(a2.scope, AddonManager.SCOPE_SYSTEM); + + await promiseShutdownManager(); +}); + +// Switching disabled locations works +add_task(async function test_disable_location2() { + Services.prefs.setIntPref( + "extensions.enabledScopes", + AddonManager.SCOPE_USER + ); + + await promiseStartupManager(); + + check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, [ID1]); + check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, [ID2]); + check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []); + + let [a1, a2] = await AddonManager.getAddonsByIDs([ID1, ID2]); + + const VERSION1 = "1.1"; + Assert.notEqual(a1, null); + Assert.equal(a1.id, ID1); + Assert.equal(a1.version, VERSION1); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID1)); + Assert.ok(isExtensionInBootstrappedList(userDir, ID1)); + Assert.ok(!hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL)); + Assert.ok(!hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE)); + do_check_in_crash_annotation(ID1, VERSION1); + Assert.equal(a1.scope, AddonManager.SCOPE_USER); + + const VERSION2 = "2.3"; + Assert.notEqual(a2, null); + Assert.equal(a2.id, ID2); + Assert.equal(a2.version, VERSION2); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID2)); + Assert.ok(isExtensionInBootstrappedList(userDir, ID2)); + Assert.ok(!isExtensionInBootstrappedList(globalDir, ID2)); + Assert.ok(!hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL)); + Assert.ok(!hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE)); + do_check_in_crash_annotation(ID2, VERSION2); + Assert.equal(a2.scope, AddonManager.SCOPE_USER); + + await promiseShutdownManager(); +}); + +// Resetting the pref makes everything visible again +add_task(async function test_enable_location() { + Services.prefs.clearUserPref("extensions.enabledScopes"); + + await promiseStartupManager(); + + check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []); + + let [a1, a2] = await AddonManager.getAddonsByIDs([ID1, ID2]); + + const VERSION1 = "1.1"; + Assert.notEqual(a1, null); + Assert.equal(a1.id, ID1); + Assert.equal(a1.version, VERSION1); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID1)); + Assert.ok(isExtensionInBootstrappedList(userDir, ID1)); + Assert.ok(!hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL)); + Assert.ok(!hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE)); + do_check_in_crash_annotation(ID1, VERSION1); + Assert.equal(a1.scope, AddonManager.SCOPE_USER); + + const VERSION2 = "2.3"; + Assert.notEqual(a2, null); + Assert.equal(a2.id, ID2); + Assert.equal(a2.version, VERSION2); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID2)); + Assert.ok(isExtensionInBootstrappedList(userDir, ID2)); + Assert.ok(!isExtensionInBootstrappedList(globalDir, ID2)); + Assert.ok(!hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL)); + Assert.ok(!hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE)); + do_check_in_crash_annotation(ID2, VERSION2); + Assert.equal(a2.scope, AddonManager.SCOPE_USER); + + await promiseShutdownManager(); +}); + +// Check that items in the profile hide the others again. +add_task(async function test_profile_hiding() { + const VERSION1 = "1.2"; + await createWebExtension(ID1, VERSION1, profileDir); + + await IOUtils.remove(PathUtils.join(userDir.path, `${ID2}.xpi`)); + + await promiseStartupManager(); + + check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, [ID1, ID2]); + check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []); + + let [a1, a2, a3] = await AddonManager.getAddonsByIDs([ID1, ID2, ID3]); + + Assert.notEqual(a1, null); + Assert.equal(a1.id, ID1); + Assert.equal(a1.version, VERSION1); + Assert.ok(isExtensionInBootstrappedList(profileDir, ID1)); + Assert.ok(!isExtensionInBootstrappedList(userDir, ID1)); + Assert.ok(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL)); + Assert.ok(hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE)); + do_check_in_crash_annotation(ID1, VERSION1); + Assert.equal(a1.scope, AddonManager.SCOPE_PROFILE); + + const VERSION2 = "2.2"; + Assert.notEqual(a2, null); + Assert.equal(a2.id, ID2); + Assert.equal(a2.version, VERSION2); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID2)); + Assert.ok(!isExtensionInBootstrappedList(userDir, ID2)); + Assert.ok(isExtensionInBootstrappedList(globalDir, ID2)); + Assert.ok(!hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL)); + Assert.ok(!hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE)); + do_check_in_crash_annotation(ID2, VERSION2); + Assert.equal(a2.scope, AddonManager.SCOPE_SYSTEM); + + Assert.equal(a3, null); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID3)); + + await promiseShutdownManager(); +}); + +// Disabling all locations still leaves the profile working +add_task(async function test_disable3() { + Services.prefs.setIntPref("extensions.enabledScopes", 0); + + await promiseStartupManager(); + + check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, [ + "2@tests.mozilla.org", + ]); + check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []); + + let [a1, a2] = await AddonManager.getAddonsByIDs([ID1, ID2]); + + const VERSION1 = "1.2"; + Assert.notEqual(a1, null); + Assert.equal(a1.id, ID1); + Assert.equal(a1.version, VERSION1); + Assert.ok(isExtensionInBootstrappedList(profileDir, ID1)); + Assert.ok(!isExtensionInBootstrappedList(userDir, ID1)); + Assert.ok(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL)); + Assert.ok(hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE)); + do_check_in_crash_annotation(ID1, VERSION1); + Assert.equal(a1.scope, AddonManager.SCOPE_PROFILE); + + Assert.equal(a2, null); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID2)); + Assert.ok(!isExtensionInBootstrappedList(userDir, ID2)); + Assert.ok(!isExtensionInBootstrappedList(globalDir, ID2)); + + await promiseShutdownManager(); +}); + +// More hiding and revealing +add_task(async function test_reval() { + Services.prefs.clearUserPref("extensions.enabledScopes"); + + await IOUtils.remove(PathUtils.join(userDir.path, `${ID1}.xpi`)); + await IOUtils.remove(PathUtils.join(globalDir.path, `${ID2}.xpi`)); + + const VERSION2 = "2.4"; + await createWebExtension(ID2, VERSION2, profileDir); + + await promiseStartupManager(); + + check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, [ + "2@tests.mozilla.org", + ]); + check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []); + + let [a1, a2, a3] = await AddonManager.getAddonsByIDs([ID1, ID2, ID3]); + + Assert.notEqual(a1, null); + Assert.equal(a1.id, ID1); + Assert.equal(a1.version, "1.2"); + Assert.ok(isExtensionInBootstrappedList(profileDir, ID1)); + Assert.ok(!isExtensionInBootstrappedList(userDir, ID1)); + Assert.ok(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL)); + Assert.ok(hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE)); + Assert.equal(a1.scope, AddonManager.SCOPE_PROFILE); + + Assert.notEqual(a2, null); + Assert.equal(a2.id, ID2); + Assert.equal(a2.version, VERSION2); + Assert.ok(isExtensionInBootstrappedList(profileDir, ID2)); + Assert.ok(!isExtensionInBootstrappedList(userDir, ID2)); + Assert.ok(!isExtensionInBootstrappedList(globalDir, ID2)); + Assert.ok(hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL)); + Assert.ok(hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE)); + Assert.equal(a2.scope, AddonManager.SCOPE_PROFILE); + + Assert.equal(a3, null); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID3)); + + await promiseShutdownManager(); +}); + +// Checks that a removal from one location and an addition in another location +// for the same item is handled +add_task(async function test_move() { + await IOUtils.remove(PathUtils.join(profileDir.path, `${ID1}.xpi`)); + const VERSION1 = "1.3"; + await createWebExtension(ID1, VERSION1, userDir); + + await promiseStartupManager(); + + check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, [ID1]); + check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []); + + let [a1, a2] = await AddonManager.getAddonsByIDs([ID1, ID2]); + + Assert.notEqual(a1, null); + Assert.equal(a1.id, ID1); + Assert.equal(a1.version, VERSION1); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID1)); + Assert.ok(isExtensionInBootstrappedList(userDir, ID1)); + Assert.ok(!hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL)); + Assert.ok(!hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE)); + Assert.equal(a1.scope, AddonManager.SCOPE_USER); + + const VERSION2 = "2.4"; + Assert.notEqual(a2, null); + Assert.equal(a2.id, ID2); + Assert.equal(a2.version, VERSION2); + Assert.ok(isExtensionInBootstrappedList(profileDir, ID2)); + Assert.ok(!isExtensionInBootstrappedList(userDir, ID2)); + Assert.ok(!isExtensionInBootstrappedList(globalDir, ID2)); + Assert.ok(hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL)); + Assert.ok(hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE)); + Assert.equal(a2.scope, AddonManager.SCOPE_PROFILE); + + await promiseShutdownManager(); +}); + +// This should remove any remaining items +add_task(async function test_remove() { + await IOUtils.remove(PathUtils.join(userDir.path, `${ID1}.xpi`)); + await IOUtils.remove(PathUtils.join(profileDir.path, `${ID2}.xpi`)); + + await promiseStartupManager(); + + check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, [ID1, ID2]); + check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []); + check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []); + + let [a1, a2, a3] = await AddonManager.getAddonsByIDs([ID1, ID2, ID3]); + Assert.equal(a1, null); + Assert.equal(a2, null); + Assert.equal(a3, null); + + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID1)); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID2)); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID3)); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID4)); + Assert.ok(!isExtensionInBootstrappedList(profileDir, ID4)); + Assert.ok(!isExtensionInBootstrappedList(userDir, ID1)); + Assert.ok(!isExtensionInBootstrappedList(userDir, ID2)); + Assert.ok(!isExtensionInBootstrappedList(userDir, ID3)); + Assert.ok(!isExtensionInBootstrappedList(userDir, ID4)); + Assert.ok(!isExtensionInBootstrappedList(userDir, ID4)); + Assert.ok(!isExtensionInBootstrappedList(globalDir, ID1)); + Assert.ok(!isExtensionInBootstrappedList(globalDir, ID2)); + Assert.ok(!isExtensionInBootstrappedList(globalDir, ID3)); + Assert.ok(!isExtensionInBootstrappedList(globalDir, ID4)); + Assert.ok(!isExtensionInBootstrappedList(globalDir, ID4)); + + await promiseShutdownManager(); +}); + +// Test that auto-disabling for specific scopes works +add_task(async function test_autoDisable() { + Services.prefs.setIntPref( + "extensions.autoDisableScopes", + AddonManager.SCOPE_USER + ); + + async function writeAll() { + return Promise.all([ + createWebExtension(ID1, "1.0", profileDir), + createWebExtension(ID2, "2.0", userDir), + createWebExtension(ID3, "3.0", globalDir), + ]); + } + + async function removeAll() { + return Promise.all([ + IOUtils.remove(PathUtils.join(profileDir.path, `${ID1}.xpi`)), + IOUtils.remove(PathUtils.join(userDir.path, `${ID2}.xpi`)), + IOUtils.remove(PathUtils.join(globalDir.path, `${ID3}.xpi`)), + ]); + } + + await writeAll(); + + await promiseStartupManager(); + + let [a1, a2, a3] = await AddonManager.getAddonsByIDs([ID1, ID2, ID3]); + Assert.notEqual(a1, null); + Assert.ok(!a1.userDisabled); + Assert.ok(a1.seen); + Assert.ok(a1.isActive); + + Assert.notEqual(a2, null); + Assert.ok(a2.userDisabled); + Assert.ok(!a2.seen); + Assert.ok(!a2.isActive); + + Assert.notEqual(a3, null); + Assert.ok(!a3.userDisabled); + Assert.ok(a3.seen); + Assert.ok(a3.isActive); + + await promiseShutdownManager(); + + await removeAll(); + + await promiseStartupManager(); + await promiseShutdownManager(); + + Services.prefs.setIntPref( + "extensions.autoDisableScopes", + AddonManager.SCOPE_SYSTEM + ); + + await writeAll(); + + await promiseStartupManager(); + + [a1, a2, a3] = await AddonManager.getAddonsByIDs([ID1, ID2, ID3]); + Assert.notEqual(a1, null); + Assert.ok(!a1.userDisabled); + Assert.ok(a1.seen); + Assert.ok(a1.isActive); + + Assert.notEqual(a2, null); + Assert.ok(!a2.userDisabled); + Assert.ok(a2.seen); + Assert.ok(a2.isActive); + + Assert.notEqual(a3, null); + Assert.ok(a3.userDisabled); + Assert.ok(!a3.seen); + Assert.ok(!a3.isActive); + + await promiseShutdownManager(); + + await removeAll(); + + await promiseStartupManager(); + await promiseShutdownManager(); + + Services.prefs.setIntPref( + "extensions.autoDisableScopes", + AddonManager.SCOPE_USER + AddonManager.SCOPE_SYSTEM + ); + + await writeAll(); + + await promiseStartupManager(); + + [a1, a2, a3] = await AddonManager.getAddonsByIDs([ID1, ID2, ID3]); + Assert.notEqual(a1, null); + Assert.ok(!a1.userDisabled); + Assert.ok(a1.seen); + Assert.ok(a1.isActive); + + Assert.notEqual(a2, null); + Assert.ok(a2.userDisabled); + Assert.ok(!a2.seen); + Assert.ok(!a2.isActive); + + Assert.notEqual(a3, null); + Assert.ok(a3.userDisabled); + Assert.ok(!a3.seen); + Assert.ok(!a3.isActive); + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_startup_enable.js b/toolkit/mozapps/extensions/test/xpcshell/test_startup_enable.js new file mode 100644 index 0000000000..a3259b599f --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_startup_enable.js @@ -0,0 +1,47 @@ +createAppInfo("xpcshell@tessts.mozilla.org", "XPCShell", "1", "1"); +BootstrapMonitor.init(); + +// Test that enabling an extension during startup generates the +// proper reason for startup(). +add_task(async function test_startup_enable() { + const ID = "compat@tests.mozilla.org"; + + await promiseStartupManager(); + + await promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { + gecko: { + id: ID, + strict_min_version: "1", + strict_max_version: "1", + }, + }, + }, + }); + + BootstrapMonitor.checkInstalled(ID); + BootstrapMonitor.checkStarted(ID); + let { reason } = BootstrapMonitor.started.get(ID); + equal( + reason, + BOOTSTRAP_REASONS.ADDON_INSTALL, + "Startup reason is ADDON_INSTALL at install" + ); + + gAppInfo.platformVersion = "2"; + await promiseRestartManager("2"); + BootstrapMonitor.checkInstalled(ID); + BootstrapMonitor.checkNotStarted(ID); + + gAppInfo.platformVersion = "1"; + await promiseRestartManager("1"); + BootstrapMonitor.checkInstalled(ID); + BootstrapMonitor.checkStarted(ID); + ({ reason } = BootstrapMonitor.started.get(ID)); + equal( + reason, + BOOTSTRAP_REASONS.ADDON_ENABLE, + "Startup reason is ADDON_ENABLE when re-enabled at startup" + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_startup_isPrivileged.js b/toolkit/mozapps/extensions/test/xpcshell/test_startup_isPrivileged.js new file mode 100644 index 0000000000..eb9cc34938 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_startup_isPrivileged.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ADDON_ID_PRIVILEGED = "@privileged-addon-id"; +const ADDON_ID_NO_PRIV = "@addon-without-privileges"; +AddonTestUtils.usePrivilegedSignatures = id => id === ADDON_ID_PRIVILEGED; + +function isExtensionPrivileged(addonId) { + const { extension } = WebExtensionPolicy.getByID(addonId); + return extension.isPrivileged; +} + +add_task(async function setup() { + await ExtensionTestUtils.startAddonManager(); +}); + +add_task(async function isPrivileged_at_install() { + { + let addon = await promiseInstallWebExtension({ + manifest: { + permissions: ["mozillaAddons"], + browser_specific_settings: { gecko: { id: ADDON_ID_PRIVILEGED } }, + }, + }); + ok(addon.isPrivileged, "Add-on is privileged"); + ok(isExtensionPrivileged(ADDON_ID_PRIVILEGED), "Extension is privileged"); + } + + { + let addon = await promiseInstallWebExtension({ + manifest: { + permissions: ["mozillaAddons"], + browser_specific_settings: { gecko: { id: ADDON_ID_NO_PRIV } }, + }, + }); + ok(!addon.isPrivileged, "Add-on is not privileged"); + ok(!isExtensionPrivileged(ADDON_ID_NO_PRIV), "Extension is not privileged"); + } +}); + +// When the Add-on Manager is restarted, the extension is started using data +// from XPIState. This test verifies that `extension.isPrivileged` is correctly +// set in that scenario. +add_task(async function isPrivileged_at_restart() { + await promiseRestartManager(); + { + let addon = await AddonManager.getAddonByID(ADDON_ID_PRIVILEGED); + ok(addon.isPrivileged, "Add-on is privileged"); + ok(isExtensionPrivileged(ADDON_ID_PRIVILEGED), "Extension is privileged"); + } + { + let addon = await AddonManager.getAddonByID(ADDON_ID_NO_PRIV); + ok(!addon.isPrivileged, "Add-on is not privileged"); + ok(!isExtensionPrivileged(ADDON_ID_NO_PRIV), "Extension is not privileged"); + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_startup_scan.js b/toolkit/mozapps/extensions/test/xpcshell/test_startup_scan.js new file mode 100644 index 0000000000..67c75cbd17 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_startup_scan.js @@ -0,0 +1,125 @@ +"use strict"; + +// Turn off startup scanning. +Services.prefs.setIntPref("extensions.startupScanScopes", 0); + +createAppInfo("xpcshell@tessts.mozilla.org", "XPCShell", "42", "42"); +// Prevent XPIStates.scanForChanges from seeing this as an update and forcing a +// full scan. +Services.prefs.setCharPref( + "extensions.lastAppBuildId", + Services.appinfo.appBuildID +); + +// A small bootstrap calls monitor targeting a single extension (created to avoid introducing a workaround +// in BootstrapMonitor to be able to test Bug 1664144 fix). +let Monitor = { + extensionId: undefined, + collected: [], + init() { + const bootstrapCallListener = (_evtName, data) => { + if (data.params.id == this.extensionId) { + this.collected.push(data); + } + }; + AddonTestUtils.on("bootstrap-method", bootstrapCallListener); + registerCleanupFunction(() => { + AddonTestUtils.off("bootstrap-method", bootstrapCallListener); + }); + }, + startCollecting(extensionId) { + this.extensionId = extensionId; + }, + stopCollecting() { + this.extensionId = undefined; + }, + getCollected() { + const collected = this.collected; + this.collected = []; + return collected; + }, +}; + +Monitor.init(); + +// Bug 1664144: Test that during startup scans, updating an addon +// that has already started is restarted. +add_task(async function test_startup_sideload_updated() { + const ID = "sideload@tests.mozilla.org"; + + await createWebExtension(ID, initialVersion("1"), profileDir); + await promiseStartupManager(); + + // Ensure the sideload is enabled and running. + let addon = await promiseAddonByID(ID); + + Monitor.startCollecting(ID); + await addon.enable(); + Monitor.stopCollecting(); + + let events = Monitor.getCollected(); + ok(events.length, "bootstrap methods called"); + equal( + events[0].reason, + BOOTSTRAP_REASONS.ADDON_ENABLE, + "Startup reason is ADDON_ENABLE at install" + ); + + await promiseShutdownManager(); + // Touch the addon on disk before startup. + await createWebExtension(ID, initialVersion("1.1"), profileDir); + Monitor.startCollecting(ID); + await promiseStartupManager(); + await AddonManagerPrivate.getNewSideloads(); + Monitor.stopCollecting(); + + events = Monitor.getCollected().map(({ method, reason, params }) => { + const { version } = params; + return { method, reason, version }; + }); + + const updatedVersion = "1.1.0"; + const expectedUpgradeParams = { + reason: BOOTSTRAP_REASONS.ADDON_UPGRADE, + version: updatedVersion, + }; + + const expectedCalls = [ + { + method: "startup", + reason: BOOTSTRAP_REASONS.APP_STARTUP, + version: "1.0", + }, + // Shutdown call has version 1.1 because the file was already + // updated on disk and got the new version as part of the startup. + { method: "shutdown", ...expectedUpgradeParams }, + { method: "update", ...expectedUpgradeParams }, + { method: "startup", ...expectedUpgradeParams }, + ]; + + for (let i = 0; i < expectedCalls.length; i++) { + Assert.deepEqual( + events[i], + expectedCalls[i], + "Got the expected sequence of bootstrap method calls" + ); + } + + equal( + events.length, + expectedCalls.length, + "Got the expected number of bootstrap method calls" + ); + + // flush addonStartup.json + await AddonTestUtils.loadAddonsList(true); + // verify startupData is correct + let startupData = aomStartup.readStartupData(); + Assert.equal( + startupData["app-profile"].addons[ID].version, + updatedVersion, + "startup data is correct in cache" + ); + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_strictcompatibility.js b/toolkit/mozapps/extensions/test/xpcshell/test_strictcompatibility.js new file mode 100644 index 0000000000..98318ceef6 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_strictcompatibility.js @@ -0,0 +1,156 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests AddonManager.strictCompatibility and it's related preference, +// extensions.strictCompatibility, and the strictCompatibility option in +// install.rdf + +PromiseTestUtils.allowMatchingRejectionsGlobally( + /IOUtils: Shutting down and refusing additional I\/O tasks/ +); +PromiseTestUtils.allowMatchingRejectionsGlobally( + /IOUtils\.profileBeforeChange getter: IOUtils: profileBeforeChange phase has already finished/ +); + +// The `compatbile` array defines which of the tests below the add-on +// should be compatible in. It's pretty gross. +const ADDONS = [ + // Always compatible + { + manifest: { + id: "addon1@tests.mozilla.org", + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1", + }, + ], + }, + compatible: { + nonStrict: true, + strict: true, + }, + }, + + // Incompatible in strict compatibility mode + { + manifest: { + id: "addon2@tests.mozilla.org", + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "0.7", + maxVersion: "0.8", + }, + ], + }, + compatible: { + nonStrict: true, + strict: false, + }, + }, + + // Opt-in to strict compatibility - always incompatible + { + manifest: { + id: "addon3@tests.mozilla.org", + strictCompatibility: true, + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "0.8", + maxVersion: "0.9", + }, + ], + }, + compatible: { + nonStrict: false, + strict: false, + }, + }, + + // Addon from the future - would be marked as compatibile-by-default, + // but minVersion is higher than the app version + { + manifest: { + id: "addon4@tests.mozilla.org", + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "3", + maxVersion: "5", + }, + ], + }, + compatible: { + nonStrict: false, + strict: false, + }, + }, + + // Dictionary - compatible even in strict compatibility mode + { + manifest: { + id: "addon5@tests.mozilla.org", + type: "dictionary", + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "0.8", + maxVersion: "0.9", + }, + ], + }, + compatible: { + nonStrict: true, + strict: true, + }, + }, +]; + +async function checkCompatStatus(strict, index) { + info(`Checking compat status for test ${index}\n`); + + equal(AddonManager.strictCompatibility, strict); + + for (let test of ADDONS) { + let { id } = test.manifest; + let addon = await promiseAddonByID(id); + checkAddon(id, addon, { + isCompatible: test.compatible[index], + appDisabled: !test.compatible[index], + }); + } +} + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + + for (let addon of ADDONS) { + let xpi = await createAddon(addon.manifest); + await manuallyInstall( + xpi, + AddonTestUtils.profileExtensions, + addon.manifest.id + ); + } + + await promiseStartupManager(); +}); + +add_task(async function test_1() { + info("Test 1"); + Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false); + await checkCompatStatus(false, "nonStrict"); + await promiseRestartManager(); + await checkCompatStatus(false, "nonStrict"); +}); + +add_task(async function test_2() { + info("Test 2"); + Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, true); + await checkCompatStatus(true, "strict"); + await promiseRestartManager(); + await checkCompatStatus(true, "strict"); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_syncGUID.js b/toolkit/mozapps/extensions/test/xpcshell/test_syncGUID.js new file mode 100644 index 0000000000..5ac50bd5d4 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_syncGUID.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const { XPIExports } = ChromeUtils.importESModule( + "resource://gre/modules/addons/XPIExports.sys.mjs" +); + +const UUID_PATTERN = + /^\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}$/i; + +const ADDONS = [ + { + id: "addon1@tests.mozilla.org", + name: "Test 1", + }, + { + id: "addon2@tests.mozilla.org", + name: "Real Test 2", + }, +]; + +let xpis; + +add_task(async function setup() { + Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9"); + await promiseStartupManager(); + + xpis = await Promise.all( + ADDONS.map(info => + createTempWebExtensionFile({ + manifest: { + name: info.name, + browser_specific_settings: { gecko: { id: info.id } }, + }, + }) + ) + ); +}); + +add_task(async function test_getter_and_setter() { + await promiseInstallFile(xpis[0]); + + let addon = await AddonManager.getAddonByID(ADDONS[0].id); + Assert.notEqual(addon, null); + Assert.notEqual(addon.syncGUID, null); + Assert.ok(UUID_PATTERN.test(addon.syncGUID)); + + let newGUID = "foo"; + + addon.syncGUID = newGUID; + Assert.equal(newGUID, addon.syncGUID); + + // Verify change made it to DB. + let newAddon = await AddonManager.getAddonByID(ADDONS[0].id); + Assert.notEqual(newAddon, null); + Assert.equal(newGUID, newAddon.syncGUID); +}); + +add_task(async function test_fetch_by_guid_unknown_guid() { + let addon = await XPIExports.XPIProvider.getAddonBySyncGUID("XXXX"); + Assert.equal(null, addon); +}); + +// Ensure setting an extension to an existing syncGUID results in error. +add_task(async function test_error_on_duplicate_syncguid_insert() { + await promiseInstallAllFiles(xpis); + + let addons = await AddonManager.getAddonsByIDs(ADDONS.map(a => a.id)); + let initialGUID = addons[1].syncGUID; + + Assert.throws( + () => { + addons[1].syncGUID = addons[0].syncGUID; + }, + /Addon sync GUID conflict/, + "Assigning conflicting sync guids throws" + ); + + await promiseRestartManager(); + + let addon = await AddonManager.getAddonByID(ADDONS[1].id); + Assert.equal(initialGUID, addon.syncGUID); +}); + +add_task(async function test_fetch_by_guid_known_guid() { + let addon = await AddonManager.getAddonByID(ADDONS[0].id); + Assert.notEqual(null, addon); + Assert.notEqual(null, addon.syncGUID); + + let syncGUID = addon.syncGUID; + + let newAddon = await XPIExports.XPIProvider.getAddonBySyncGUID(syncGUID); + Assert.notEqual(null, newAddon); + Assert.equal(syncGUID, newAddon.syncGUID); +}); + +add_task(async function test_addon_manager_get_by_sync_guid() { + let addon = await AddonManager.getAddonByID(ADDONS[0].id); + Assert.notEqual(null, addon.syncGUID); + + let syncGUID = addon.syncGUID; + + let newAddon = await AddonManager.getAddonBySyncGUID(syncGUID); + Assert.notEqual(null, newAddon); + Assert.equal(addon.id, newAddon.id); + Assert.equal(syncGUID, newAddon.syncGUID); + + let missing = await AddonManager.getAddonBySyncGUID("DOES_NOT_EXIST"); + Assert.equal(undefined, missing); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_allowed.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_allowed.js new file mode 100644 index 0000000000..996f334382 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_allowed.js @@ -0,0 +1,55 @@ +// Tests that only allowed built-in system add-ons are loaded on startup. + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "0"); + +// Ensure that only allowed add-ons are loaded. +add_task(async function test_allowed_addons() { + // Build the test set + var distroDir = FileUtils.getDir("ProfD", ["sysfeatures"]); + distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + let xpi = await getSystemAddonXPI(1, "1.0"); + xpi.copyTo(distroDir, "system1@tests.mozilla.org.xpi"); + + xpi = await getSystemAddonXPI(2, "1.0"); + xpi.copyTo(distroDir, "system2@tests.mozilla.org.xpi"); + + xpi = await getSystemAddonXPI(3, "1.0"); + xpi.copyTo(distroDir, "system3@tests.mozilla.org.xpi"); + + registerDirectory("XREAppFeat", distroDir); + + // 1 and 2 are allowed, 3 is not. + let validAddons = { + system: ["system1@tests.mozilla.org", "system2@tests.mozilla.org"], + }; + await overrideBuiltIns(validAddons); + + await promiseStartupManager(); + + let addon = await AddonManager.getAddonByID("system1@tests.mozilla.org"); + notEqual(addon, null); + + addon = await AddonManager.getAddonByID("system2@tests.mozilla.org"); + notEqual(addon, null); + + addon = await AddonManager.getAddonByID("system3@tests.mozilla.org"); + Assert.equal(addon, null); + equal(addon, null); + + // 3 is now allowed, 1 and 2 are not. + validAddons = { system: ["system3@tests.mozilla.org"] }; + await overrideBuiltIns(validAddons); + + await promiseRestartManager(); + + addon = await AddonManager.getAddonByID("system1@tests.mozilla.org"); + equal(addon, null); + + addon = await AddonManager.getAddonByID("system2@tests.mozilla.org"); + equal(addon, null); + + addon = await AddonManager.getAddonByID("system3@tests.mozilla.org"); + notEqual(addon, null); + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_delay_update.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_delay_update.js new file mode 100644 index 0000000000..4490ec065b --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_delay_update.js @@ -0,0 +1,486 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies that delaying a system add-on update works. + +PromiseTestUtils.allowMatchingRejectionsGlobally( + /Message manager disconnected/ +); + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +const IGNORE_ID = "system_delay_ignore@tests.mozilla.org"; +const COMPLETE_ID = "system_delay_complete@tests.mozilla.org"; +const DEFER_ID = "system_delay_defer@tests.mozilla.org"; +const DEFER2_ID = "system_delay_defer2@tests.mozilla.org"; +const DEFER_ALSO_ID = "system_delay_defer_also@tests.mozilla.org"; +const NORMAL_ID = "system1@tests.mozilla.org"; + +const distroDir = FileUtils.getDir("ProfD", ["sysfeatures"]); +distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", distroDir); + +registerCleanupFunction(() => { + distroDir.remove(true); +}); + +AddonTestUtils.usePrivilegedSignatures = id => "system"; + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +function promiseInstallPostponed(addonID1, addonID2) { + return new Promise((resolve, reject) => { + let seen = []; + let listener = { + onInstallFailed: () => { + AddonManager.removeInstallListener(listener); + reject("extension installation should not have failed"); + }, + onInstallEnded: install => { + AddonManager.removeInstallListener(listener); + reject( + `extension installation should not have ended for ${install.addon.id}` + ); + }, + onInstallPostponed: install => { + seen.push(install.addon.id); + if (seen.includes(addonID1) && seen.includes(addonID2)) { + AddonManager.removeInstallListener(listener); + resolve(); + } + }, + }; + + AddonManager.addInstallListener(listener); + }); +} + +function promiseInstallResumed(addonID1, addonID2) { + return new Promise((resolve, reject) => { + let seenPostponed = []; + let seenEnded = []; + let listener = { + onInstallFailed: () => { + AddonManager.removeInstallListener(listener); + reject("extension installation should not have failed"); + }, + onInstallEnded: install => { + seenEnded.push(install.addon.id); + if ( + seenEnded.includes(addonID1) && + seenEnded.includes(addonID2) && + seenPostponed.includes(addonID1) && + seenPostponed.includes(addonID2) + ) { + AddonManager.removeInstallListener(listener); + resolve(); + } + }, + onInstallPostponed: install => { + seenPostponed.push(install.addon.id); + }, + }; + + AddonManager.addInstallListener(listener); + }); +} + +function promiseInstallDeferred(addonID1, addonID2) { + return new Promise((resolve, reject) => { + let seenEnded = []; + let listener = { + onInstallFailed: () => { + AddonManager.removeInstallListener(listener); + reject("extension installation should not have failed"); + }, + onInstallEnded: install => { + seenEnded.push(install.addon.id); + if (seenEnded.includes(addonID1) && seenEnded.includes(addonID2)) { + AddonManager.removeInstallListener(listener); + resolve(); + } + }, + }; + + AddonManager.addInstallListener(listener); + }); +} + +async function checkAddon(addonID, { version }) { + let addon = await promiseAddonByID(addonID); + Assert.notEqual(addon, null); + Assert.equal(addon.version, version); + Assert.ok(addon.isCompatible); + Assert.ok(!addon.appDisabled); + Assert.ok(addon.isActive); + Assert.equal(addon.type, "extension"); +} + +// Tests below have webextension background scripts inline. +/* globals browser */ + +// add-on registers upgrade listener, and ignores update. +add_task(async function test_addon_upgrade_on_restart() { + // discard system addon updates + Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, ""); + + let xpi = await getSystemAddonXPI(1, "1.0"); + xpi.copyTo(distroDir, `${NORMAL_ID}.xpi`); + + // Version 1.0 of an extension that ignores updates. + function background() { + browser.runtime.onUpdateAvailable.addListener(() => { + browser.test.sendMessage("got-update"); + }); + } + + xpi = await createTempWebExtensionFile({ + background, + + manifest: { + version: "1.0", + browser_specific_settings: { gecko: { id: IGNORE_ID } }, + }, + }); + xpi.copyTo(distroDir, `${IGNORE_ID}.xpi`); + + // Version 2.0 of the same extension. + let xpi2 = await createTempWebExtensionFile({ + manifest: { + version: "2.0", + browser_specific_settings: { gecko: { id: IGNORE_ID } }, + }, + }); + + await overrideBuiltIns({ system: [IGNORE_ID, NORMAL_ID] }); + + let extension = ExtensionTestUtils.expectExtension(IGNORE_ID); + + await Promise.all([promiseStartupManager(), extension.awaitStartup()]); + + let updateList = [ + { + id: IGNORE_ID, + version: "2.0", + path: "system_delay_ignore_2.xpi", + xpi: xpi2, + }, + { + id: NORMAL_ID, + version: "2.0", + path: "system1_2.xpi", + xpi: await getSystemAddonXPI(1, "2.0"), + }, + ]; + + await Promise.all([ + promiseInstallPostponed(IGNORE_ID, NORMAL_ID), + installSystemAddons(buildSystemAddonUpdates(updateList)), + extension.awaitMessage("got-update"), + ]); + + // addon upgrade has been delayed. + await checkAddon(IGNORE_ID, { version: "1.0" }); + // other addons in the set are delayed as well. + await checkAddon(NORMAL_ID, { version: "1.0" }); + + // restarting allows upgrades to proceed + await Promise.all([promiseRestartManager(), extension.awaitStartup()]); + + await checkAddon(IGNORE_ID, { version: "2.0" }); + await checkAddon(NORMAL_ID, { version: "2.0" }); + + await promiseShutdownManager(); +}); + +// add-on registers upgrade listener, and allows update. +add_task(async function test_addon_upgrade_on_reload() { + // discard system addon updates + Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, ""); + + let xpi = await getSystemAddonXPI(1, "1.0"); + xpi.copyTo(distroDir, `${NORMAL_ID}.xpi`); + + // Version 1.0 of an extension that listens for and immediately + // applies updates. + function background() { + browser.runtime.onUpdateAvailable.addListener(function listener() { + browser.runtime.onUpdateAvailable.removeListener(listener); + browser.test.sendMessage("got-update"); + browser.runtime.reload(); + }); + } + + xpi = await createTempWebExtensionFile({ + background, + + manifest: { + version: "1.0", + browser_specific_settings: { gecko: { id: COMPLETE_ID } }, + }, + }); + xpi.copyTo(distroDir, `${COMPLETE_ID}.xpi`); + + // Version 2.0 of the same extension. + let xpi2 = await createTempWebExtensionFile({ + manifest: { + version: "2.0", + browser_specific_settings: { gecko: { id: COMPLETE_ID } }, + }, + }); + + await overrideBuiltIns({ system: [COMPLETE_ID, NORMAL_ID] }); + + let extension = ExtensionTestUtils.expectExtension(COMPLETE_ID); + + await Promise.all([promiseStartupManager(), extension.awaitStartup()]); + + let updateList = [ + { + id: COMPLETE_ID, + version: "2.0", + path: "system_delay_complete_2.xpi", + xpi: xpi2, + }, + { + id: NORMAL_ID, + version: "2.0", + path: "system1_2.xpi", + xpi: await getSystemAddonXPI(1, "2.0"), + }, + ]; + + // initial state + await checkAddon(COMPLETE_ID, { version: "1.0" }); + await checkAddon(NORMAL_ID, { version: "1.0" }); + + // We should see that the onUpdateListener executed, then see the + // update resume. + await Promise.all([ + extension.awaitMessage("got-update"), + promiseInstallResumed(COMPLETE_ID, NORMAL_ID), + installSystemAddons(buildSystemAddonUpdates(updateList)), + ]); + await extension.awaitStartup(); + + // addon upgrade has been allowed + await checkAddon(COMPLETE_ID, { version: "2.0" }); + // other upgrades in the set are allowed as well + await checkAddon(NORMAL_ID, { version: "2.0" }); + + // restarting changes nothing + await Promise.all([promiseRestartManager(), extension.awaitStartup()]); + + await checkAddon(COMPLETE_ID, { version: "2.0" }); + await checkAddon(NORMAL_ID, { version: "2.0" }); + + await promiseShutdownManager(); +}); + +function delayBackground() { + browser.test.onMessage.addListener(msg => { + if (msg !== "reload") { + browser.test.fail(`Got unexpected test message: ${msg}`); + } + browser.runtime.reload(); + }); + browser.runtime.onUpdateAvailable.addListener(async function listener() { + browser.runtime.onUpdateAvailable.removeListener(listener); + browser.test.sendMessage("got-update"); + }); +} + +// Upgrade listener initially defers then proceeds after a pause. +add_task(async function test_addon_upgrade_after_pause() { + // discard system addon updates + Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, ""); + + let xpi = await getSystemAddonXPI(1, "1.0"); + xpi.copyTo(distroDir, `${NORMAL_ID}.xpi`); + + // Version 1.0 of an extension that delays upgrades. + xpi = await createTempWebExtensionFile({ + background: delayBackground, + manifest: { + version: "1.0", + browser_specific_settings: { gecko: { id: DEFER_ID } }, + }, + }); + xpi.copyTo(distroDir, `${DEFER_ID}.xpi`); + + // Version 2.0 of the same xtension. + let xpi2 = await createTempWebExtensionFile({ + manifest: { + version: "2.0", + browser_specific_settings: { gecko: { id: DEFER_ID } }, + }, + }); + + await overrideBuiltIns({ system: [DEFER_ID, NORMAL_ID] }); + + let extension = ExtensionTestUtils.expectExtension(DEFER_ID); + + await Promise.all([promiseStartupManager(), extension.awaitStartup()]); + + let updateList = [ + { + id: DEFER_ID, + version: "2.0", + path: "system_delay_defer_2.xpi", + xpi: xpi2, + }, + { + id: NORMAL_ID, + version: "2.0", + path: "system1_2.xpi", + xpi: await getSystemAddonXPI(1, "2.0"), + }, + ]; + + await Promise.all([ + promiseInstallPostponed(DEFER_ID, NORMAL_ID), + installSystemAddons(buildSystemAddonUpdates(updateList)), + extension.awaitMessage("got-update"), + ]); + + // upgrade is initially postponed + await checkAddon(DEFER_ID, { version: "1.0" }); + // other addons in the set are postponed as well. + await checkAddon(NORMAL_ID, { version: "1.0" }); + + let deferred = promiseInstallDeferred(DEFER_ID, NORMAL_ID); + + // Tell the extension to proceed with the update. + extension.setRestarting(); + extension.sendMessage("reload"); + + await Promise.all([deferred, extension.awaitStartup()]); + + // addon upgrade has been allowed + await checkAddon(DEFER_ID, { version: "2.0" }); + // other addons in the set are allowed as well. + await checkAddon(NORMAL_ID, { version: "2.0" }); + + // restarting changes nothing + await promiseRestartManager(); + await extension.awaitStartup(); + + await checkAddon(DEFER_ID, { version: "2.0" }); + await checkAddon(NORMAL_ID, { version: "2.0" }); + + await promiseShutdownManager(); +}); + +// Multiple add-ons register update listeners, initially defers then +// each unblock in turn. +add_task(async function test_multiple_addon_upgrade_postpone() { + // discard system addon updates. + Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, ""); + + let updateList = []; + + let xpi = await createTempWebExtensionFile({ + background: delayBackground, + manifest: { + version: "1.0", + browser_specific_settings: { gecko: { id: DEFER2_ID } }, + }, + }); + xpi.copyTo(distroDir, `${DEFER2_ID}.xpi`); + + xpi = await createTempWebExtensionFile({ + manifest: { + version: "2.0", + browser_specific_settings: { gecko: { id: DEFER2_ID } }, + }, + }); + updateList.push({ + id: DEFER2_ID, + version: "2.0", + path: "system_delay_defer_2.xpi", + xpi, + }); + + xpi = await createTempWebExtensionFile({ + background: delayBackground, + manifest: { + version: "1.0", + browser_specific_settings: { gecko: { id: DEFER_ALSO_ID } }, + }, + }); + xpi.copyTo(distroDir, `${DEFER_ALSO_ID}.xpi`); + + xpi = await createTempWebExtensionFile({ + manifest: { + version: "2.0", + browser_specific_settings: { gecko: { id: DEFER_ALSO_ID } }, + }, + }); + updateList.push({ + id: DEFER_ALSO_ID, + version: "2.0", + path: "system_delay_defer_also_2.xpi", + xpi, + }); + + await overrideBuiltIns({ system: [DEFER2_ID, DEFER_ALSO_ID] }); + + let extension1 = ExtensionTestUtils.expectExtension(DEFER2_ID); + let extension2 = ExtensionTestUtils.expectExtension(DEFER_ALSO_ID); + + await Promise.all([ + promiseStartupManager(), + extension1.awaitStartup(), + extension2.awaitStartup(), + ]); + + await Promise.all([ + promiseInstallPostponed(DEFER2_ID, DEFER_ALSO_ID), + installSystemAddons(buildSystemAddonUpdates(updateList)), + extension1.awaitMessage("got-update"), + extension2.awaitMessage("got-update"), + ]); + + // upgrade is initially postponed + await checkAddon(DEFER2_ID, { version: "1.0" }); + // other addons in the set are postponed as well. + await checkAddon(DEFER_ALSO_ID, { version: "1.0" }); + + let deferred = promiseInstallDeferred(DEFER2_ID, DEFER_ALSO_ID); + + // Let one extension request that the update proceed. + extension1.setRestarting(); + extension1.sendMessage("reload"); + + // Upgrade blockers still present. + await checkAddon(DEFER2_ID, { version: "1.0" }); + await checkAddon(DEFER_ALSO_ID, { version: "1.0" }); + + // Let the second extension allow the update to proceed. + extension2.setRestarting(); + extension2.sendMessage("reload"); + + await Promise.all([ + deferred, + extension1.awaitStartup(), + extension2.awaitStartup(), + ]); + + // addon upgrade has been allowed + await checkAddon(DEFER2_ID, { version: "2.0" }); + await checkAddon(DEFER_ALSO_ID, { version: "2.0" }); + + // restarting changes nothing + await Promise.all([ + promiseRestartManager(), + extension1.awaitStartup(), + extension2.awaitStartup(), + ]); + + await checkAddon(DEFER2_ID, { version: "2.0" }); + await checkAddon(DEFER_ALSO_ID, { version: "2.0" }); + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_profile_location.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_profile_location.js new file mode 100644 index 0000000000..9dbf8f7070 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_profile_location.js @@ -0,0 +1,204 @@ +"use strict"; + +/* globals browser */ +let scopes = AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION; +Services.prefs.setIntPref("extensions.enabledScopes", scopes); + +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "1" +); + +AddonTestUtils.usePrivilegedSignatures = id => id.startsWith("system"); + +async function promiseInstallSystemProfileExtension(id, hidden) { + let xpi = await AddonTestUtils.createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { gecko: { id } }, + hidden, + }, + background() { + browser.test.sendMessage("started"); + }, + }); + let wrapper = ExtensionTestUtils.expectExtension(id); + + const install = await AddonManager.getInstallForURL(`file://${xpi.path}`, { + useSystemLocation: true, // KEY_APP_SYSTEM_PROFILE + }); + + install.install(); + + await wrapper.awaitStartup(); + await wrapper.awaitMessage("started"); + return wrapper; +} + +async function promiseUninstall(id) { + await AddonManager.uninstallSystemProfileAddon(id); + + let addon = await promiseAddonByID(id); + equal(addon, null, "Addon is gone after uninstall"); +} + +// Tests installing an extension into the app-system-profile location. +add_task(async function test_system_profile_location() { + let id = "system@tests.mozilla.org"; + await AddonTestUtils.promiseStartupManager(); + let wrapper = await promiseInstallSystemProfileExtension(id); + + let addon = await promiseAddonByID(id); + notEqual(addon, null, "Addon is installed"); + ok(addon.isActive, "Addon is active"); + ok(addon.isPrivileged, "Addon is privileged"); + ok(wrapper.extension.isAppProvided, "Addon is app provided"); + ok(!addon.hidden, "Addon is not hidden"); + equal( + addon.signedState, + AddonManager.SIGNEDSTATE_PRIVILEGED, + "Addon is system signed" + ); + + // After a restart, the extension should start up normally. + await promiseRestartManager(); + await wrapper.awaitStartup(); + await wrapper.awaitMessage("started"); + ok(true, "Extension in app-system-profile location ran after restart"); + + addon = await promiseAddonByID(id); + notEqual(addon, null, "Addon is installed"); + ok(addon.isActive, "Addon is active"); + + // After a restart that causes a database rebuild, it should still work + await promiseRestartManager("2"); + await wrapper.awaitStartup(); + await wrapper.awaitMessage("started"); + ok(true, "Extension in app-system-profile location ran after restart"); + + addon = await promiseAddonByID(id); + notEqual(addon, null, "Addon is installed"); + ok(addon.isActive, "Addon is active"); + + // After a restart that changes the schema version, it should still work + await promiseShutdownManager(); + Services.prefs.setIntPref("extensions.databaseSchema", 0); + await promiseStartupManager(); + + await wrapper.awaitStartup(); + await wrapper.awaitMessage("started"); + ok(true, "Extension in app-system-profile location ran after restart"); + + addon = await promiseAddonByID(id); + notEqual(addon, null, "Addon is installed"); + ok(addon.isActive, "Addon is active"); + + await promiseUninstall(id); + + await AddonTestUtils.promiseShutdownManager(); +}); + +// Tests installing a hidden extension in the app-system-profile location. +add_task(async function test_system_profile_location_hidden() { + let id = "system-hidden@tests.mozilla.org"; + await AddonTestUtils.promiseStartupManager(); + await promiseInstallSystemProfileExtension(id, true); + + let addon = await promiseAddonByID(id); + notEqual(addon, null, "Addon is installed"); + ok(addon.isActive, "Addon is active"); + ok(addon.isPrivileged, "Addon is privileged"); + ok(addon.hidden, "Addon is hidden"); + + await promiseUninstall(id); + + await AddonTestUtils.promiseShutdownManager(); +}); + +add_task(async function test_system_profile_location_installFile() { + await AddonTestUtils.promiseStartupManager(); + let id = "system-fileinstall@test"; + let xpi = await AddonTestUtils.createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { gecko: { id } }, + }, + background() { + browser.test.sendMessage("started"); + }, + }); + let wrapper = ExtensionTestUtils.expectExtension(id); + + const install = await AddonManager.getInstallForFile(xpi, null, null, true); + install.install(); + + await wrapper.awaitStartup(); + await wrapper.awaitMessage("started"); + + await promiseUninstall(id); + + await AddonTestUtils.promiseShutdownManager(); +}); + +add_task(async function test_system_profile_location_overridden() { + await AddonTestUtils.promiseStartupManager(); + let id = "system-fileinstall@test"; + let xpi = await AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version: "1.0", + browser_specific_settings: { gecko: { id } }, + }, + }); + + let install = await AddonManager.getInstallForFile(xpi, null, null, true); + await install.install(); + + let addon = await promiseAddonByID(id); + equal(addon.version, "1.0", "Addon is installed"); + + // Install a user profile version on top of the system profile location. + xpi = await AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version: "2.0", + browser_specific_settings: { gecko: { id } }, + }, + }); + + install = await AddonManager.getInstallForFile(xpi); + await install.install(); + + addon = await promiseAddonByID(id); + equal(addon.version, "2.0", "Addon is upgraded"); + + // Uninstall the system profile addon. + await AddonManager.uninstallSystemProfileAddon(id); + + addon = await promiseAddonByID(id); + equal(addon.version, "2.0", "Addon is still active"); + + await addon.uninstall(); + await AddonTestUtils.promiseShutdownManager(); +}); + +add_task(async function test_system_profile_location_require_system_cert() { + await AddonTestUtils.promiseStartupManager(); + let id = "fail@test"; + let xpi = await AddonTestUtils.createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { gecko: { id } }, + }, + }); + const install = await AddonManager.getInstallForURL(`file://${xpi.path}`, { + useSystemLocation: true, // KEY_APP_SYSTEM_PROFILE + }); + + await install.install(); + + let addon = await promiseAddonByID(id); + ok(!addon.isPrivileged, "Addon is not privileged"); + ok(!addon.isActive, "Addon is not active"); + equal(addon.signedState, AddonManager.SIGNEDSTATE_SIGNED, "Addon is signed"); + + await addon.uninstall(); + await AddonTestUtils.promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_repository.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_repository.js new file mode 100644 index 0000000000..dc1c96e7bc --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_repository.js @@ -0,0 +1,69 @@ +// Tests that AddonRepository doesn't download results for system add-ons + +const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled"; + +var gServer = new HttpServer(); +gServer.start(-1); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "0"); + +// Test with a missing features directory +add_task(async function test_app_addons() { + // Build the test set + var distroDir = FileUtils.getDir("ProfD", ["sysfeatures"]); + distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + let xpi = await getSystemAddonXPI(1, "1.0"); + xpi.copyTo(distroDir, "system1@tests.mozilla.org.xpi"); + + xpi = await getSystemAddonXPI(2, "1.0"); + xpi.copyTo(distroDir, "system2@tests.mozilla.org.xpi"); + + xpi = await getSystemAddonXPI(3, "1.0"); + xpi.copyTo(distroDir, "system3@tests.mozilla.org.xpi"); + + registerDirectory("XREAppFeat", distroDir); + + Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true); + Services.prefs.setCharPref( + PREF_GETADDONS_BYIDS, + `http://localhost:${gServer.identity.primaryPort}/get?%IDS%` + ); + + gServer.registerPathHandler("/get", (request, response) => { + do_throw("Unexpected request to server."); + }); + + await overrideBuiltIns({ + system: [ + "system1@tests.mozilla.org", + "system2@tests.mozilla.org", + "system3@tests.mozilla.org", + ], + }); + + await promiseStartupManager(); + + await AddonRepository.cacheAddons([ + "system1@tests.mozilla.org", + "system2@tests.mozilla.org", + "system3@tests.mozilla.org", + ]); + + let cached = await AddonRepository.getCachedAddonByID( + "system1@tests.mozilla.org" + ); + Assert.equal(cached, null); + + cached = await AddonRepository.getCachedAddonByID( + "system2@tests.mozilla.org" + ); + Assert.equal(cached, null); + + cached = await AddonRepository.getCachedAddonByID( + "system3@tests.mozilla.org" + ); + Assert.equal(cached, null); + + await promiseShutdownManager(); + await new Promise(resolve => gServer.stop(resolve)); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js new file mode 100644 index 0000000000..93e4c516fa --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js @@ -0,0 +1,539 @@ +// Tests that we reset to the default system add-ons correctly when switching +// application versions + +const updatesDir = FileUtils.getDir("ProfD", ["features"]); + +AddonTestUtils.usePrivilegedSignatures = id => "system"; + +add_task(async function setup() { + // Build the test sets + let dir = FileUtils.getDir("ProfD", ["sysfeatures", "app1"]); + dir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + let xpi = await getSystemAddonXPI(1, "1.0"); + xpi.copyTo(dir, "system1@tests.mozilla.org.xpi"); + + xpi = await getSystemAddonXPI(2, "1.0"); + xpi.copyTo(dir, "system2@tests.mozilla.org.xpi"); + + dir = FileUtils.getDir("ProfD", ["sysfeatures", "app2"]); + dir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + xpi = await getSystemAddonXPI(1, "2.0"); + xpi.copyTo(dir, "system1@tests.mozilla.org.xpi"); + + xpi = await getSystemAddonXPI(3, "1.0"); + xpi.copyTo(dir, "system3@tests.mozilla.org.xpi"); + + dir = FileUtils.getDir("ProfD", ["sysfeatures", "app3"]); + dir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + xpi = await getSystemAddonXPI(1, "1.0"); + xpi.copyTo(dir, "system1@tests.mozilla.org.xpi"); + + xpi = await getSystemAddonXPI(3, "1.0"); + xpi.copyTo(dir, "system3@tests.mozilla.org.xpi"); +}); + +const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"]); +distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", distroDir); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "0"); + +function makeUUID() { + let uuidGen = Services.uuid; + return uuidGen.generateUUID().toString(); +} + +async function check_installed(conditions) { + for (let i = 0; i < conditions.length; i++) { + let condition = conditions[i]; + let id = "system" + (i + 1) + "@tests.mozilla.org"; + let addon = await promiseAddonByID(id); + + if (!("isUpgrade" in condition) || !("version" in condition)) { + throw Error("condition must contain isUpgrade and version"); + } + let isUpgrade = conditions[i].isUpgrade; + let version = conditions[i].version; + + let expectedDir = isUpgrade ? updatesDir : distroDir; + + if (version) { + // Add-on should be installed + Assert.notEqual(addon, null); + Assert.equal(addon.version, version); + Assert.ok(addon.isActive); + Assert.ok(!addon.foreignInstall); + Assert.ok(addon.hidden); + Assert.ok(addon.isSystem); + Assert.ok(!hasFlag(addon.permissions, AddonManager.PERM_CAN_UPGRADE)); + if (isUpgrade) { + Assert.ok( + hasFlag(addon.permissions, AddonManager.PERM_API_CAN_UNINSTALL) + ); + } else { + Assert.ok( + !hasFlag(addon.permissions, AddonManager.PERM_API_CAN_UNINSTALL) + ); + } + + // Verify the add-ons file is in the right place + let file = expectedDir.clone(); + file.append(id + ".xpi"); + Assert.ok(file.exists()); + Assert.ok(file.isFile()); + + Assert.equal(getAddonFile(addon).path, file.path); + + if (isUpgrade) { + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_SYSTEM); + } + } else if (isUpgrade) { + // Add-on should not be installed + Assert.equal(addon, null); + } else { + // Either add-on should not be installed or it shouldn't be active + Assert.ok(!addon || !addon.isActive); + } + } +} + +// Test with a missing features directory +add_task(async function test_missing_app_dir() { + await overrideBuiltIns({ + system: [ + "system1@tests.mozilla.org", + "system2@tests.mozilla.org", + "system3@tests.mozilla.org", + "system5@tests.mozilla.org", + ], + }); + await promiseStartupManager(); + + let conditions = [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ]; + + await check_installed(conditions); + + Assert.ok(!updatesDir.exists()); + + await promiseShutdownManager(); +}); + +// Add some features in a new version +add_task(async function test_new_version() { + gAppInfo.version = "1"; + distroDir.leafName = "app1"; + await overrideBuiltIns({ + system: [ + "system1@tests.mozilla.org", + "system2@tests.mozilla.org", + "system3@tests.mozilla.org", + "system5@tests.mozilla.org", + ], + }); + await promiseStartupManager(); + + let conditions = [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: null }, + ]; + + await check_installed(conditions); + + Assert.ok(!updatesDir.exists()); + + await promiseShutdownManager(); +}); + +// Another new version swaps one feature and upgrades another +add_task(async function test_upgrade() { + gAppInfo.version = "2"; + distroDir.leafName = "app2"; + await overrideBuiltIns({ + system: [ + "system1@tests.mozilla.org", + "system2@tests.mozilla.org", + "system3@tests.mozilla.org", + "system5@tests.mozilla.org", + ], + }); + await promiseStartupManager(); + + let conditions = [ + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: "1.0" }, + ]; + + await check_installed(conditions); + + Assert.ok(!updatesDir.exists()); + + await promiseShutdownManager(); +}); + +// Downgrade +add_task(async function test_downgrade() { + gAppInfo.version = "1"; + distroDir.leafName = "app1"; + await overrideBuiltIns({ + system: [ + "system1@tests.mozilla.org", + "system2@tests.mozilla.org", + "system3@tests.mozilla.org", + "system5@tests.mozilla.org", + ], + }); + await promiseStartupManager(); + + let conditions = [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: null }, + ]; + + await check_installed(conditions); + + Assert.ok(!updatesDir.exists()); + + await promiseShutdownManager(); +}); + +// Fake a mid-cycle install +add_task(async function test_updated() { + // Create a random dir to install into + let dirname = makeUUID(); + let dir = FileUtils.getDir("ProfD", ["features", dirname]); + dir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + updatesDir.append(dirname); + + // Copy in the system add-ons + let file = await getSystemAddonXPI(2, "2.0"); + file.copyTo(updatesDir, "system2@tests.mozilla.org.xpi"); + file = await getSystemAddonXPI(3, "2.0"); + file.copyTo(updatesDir, "system3@tests.mozilla.org.xpi"); + + // Inject it into the system set + let addonSet = { + schema: 1, + directory: updatesDir.leafName, + addons: { + "system2@tests.mozilla.org": { + version: "2.0", + }, + "system3@tests.mozilla.org": { + version: "2.0", + }, + }, + }; + Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, JSON.stringify(addonSet)); + + await overrideBuiltIns({ + system: [ + "system1@tests.mozilla.org", + "system2@tests.mozilla.org", + "system3@tests.mozilla.org", + "system5@tests.mozilla.org", + ], + }); + await promiseStartupManager(); + + let conditions = [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + ]; + + await check_installed(conditions); + + await promiseShutdownManager(); +}); + +// Entering safe mode should disable the updated system add-ons and use the +// default system add-ons +add_task(async function safe_mode_disabled() { + gAppInfo.inSafeMode = true; + await overrideBuiltIns({ + system: [ + "system1@tests.mozilla.org", + "system2@tests.mozilla.org", + "system3@tests.mozilla.org", + "system5@tests.mozilla.org", + ], + }); + await promiseStartupManager(); + + let conditions = [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: null }, + ]; + + await check_installed(conditions); + + await promiseShutdownManager(); +}); + +// Leaving safe mode should re-enable the updated system add-ons +add_task(async function normal_mode_enabled() { + gAppInfo.inSafeMode = false; + await overrideBuiltIns({ + system: [ + "system1@tests.mozilla.org", + "system2@tests.mozilla.org", + "system3@tests.mozilla.org", + "system5@tests.mozilla.org", + ], + }); + await promiseStartupManager(); + + let conditions = [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + ]; + + await check_installed(conditions); + + await promiseShutdownManager(); +}); + +// An additional add-on in the directory should be ignored +add_task(async function test_skips_additional() { + // Copy in the system add-ons + let file = await getSystemAddonXPI(4, "1.0"); + file.copyTo(updatesDir, "system4@tests.mozilla.org.xpi"); + + await overrideBuiltIns({ + system: [ + "system1@tests.mozilla.org", + "system2@tests.mozilla.org", + "system3@tests.mozilla.org", + "system5@tests.mozilla.org", + ], + }); + await promiseStartupManager(); + + let conditions = [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + ]; + + await check_installed(conditions); + + await promiseShutdownManager(); +}); + +// Missing add-on should revert to the default set +add_task(async function test_revert() { + manuallyUninstall(updatesDir, "system2@tests.mozilla.org"); + + await overrideBuiltIns({ + system: [ + "system1@tests.mozilla.org", + "system2@tests.mozilla.org", + "system3@tests.mozilla.org", + "system5@tests.mozilla.org", + ], + }); + await promiseStartupManager(); + + // With system add-on 2 gone the updated set is now invalid so it reverts to + // the default set which is system add-ons 1 and 2. + let conditions = [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: null }, + ]; + + await check_installed(conditions); + + await promiseShutdownManager(); +}); + +// Putting it back will make the set work again +add_task(async function test_reuse() { + let file = await getSystemAddonXPI(2, "2.0"); + file.copyTo(updatesDir, "system2@tests.mozilla.org.xpi"); + + await overrideBuiltIns({ + system: [ + "system1@tests.mozilla.org", + "system2@tests.mozilla.org", + "system3@tests.mozilla.org", + "system5@tests.mozilla.org", + ], + }); + await promiseStartupManager(); + + let conditions = [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + ]; + + await check_installed(conditions); + + await promiseShutdownManager(); +}); + +// Making the pref corrupt should revert to the default set +add_task(async function test_corrupt_pref() { + Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, "foo"); + + await overrideBuiltIns({ + system: [ + "system1@tests.mozilla.org", + "system2@tests.mozilla.org", + "system3@tests.mozilla.org", + "system5@tests.mozilla.org", + ], + }); + await promiseStartupManager(); + + let conditions = [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: null }, + ]; + + await check_installed(conditions); + + await promiseShutdownManager(); +}); + +// An add-on with a bad certificate should cause us to use the default set +add_task(async function test_bad_profile_cert() { + let file = await getSystemAddonXPI(1, "1.0"); + file.copyTo(updatesDir, "system1@tests.mozilla.org.xpi"); + + // Inject it into the system set + let addonSet = { + schema: 1, + directory: updatesDir.leafName, + addons: { + "system1@tests.mozilla.org": { + version: "2.0", + }, + "system2@tests.mozilla.org": { + version: "1.0", + }, + "system3@tests.mozilla.org": { + version: "1.0", + }, + }, + }; + Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, JSON.stringify(addonSet)); + + await overrideBuiltIns({ + system: [ + "system1@tests.mozilla.org", + "system2@tests.mozilla.org", + "system3@tests.mozilla.org", + "system5@tests.mozilla.org", + ], + }); + await promiseStartupManager(); + + let conditions = [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: null }, + ]; + + await check_installed(conditions); + + await promiseShutdownManager(); +}); + +// Switching to app defaults that contain a bad certificate should still work +add_task(async function test_bad_app_cert() { + gAppInfo.version = "3"; + distroDir.leafName = "app3"; + + AddonTestUtils.usePrivilegedSignatures = id => { + return id === "system1@tests.mozilla.org" ? false : "system"; + }; + + await overrideBuiltIns({ + system: [ + "system1@tests.mozilla.org", + "system2@tests.mozilla.org", + "system3@tests.mozilla.org", + "system5@tests.mozilla.org", + ], + }); + await promiseStartupManager(); + + // Since we updated the app version, the system addon set should be reset as well. + let addonSet = Services.prefs.getCharPref(PREF_SYSTEM_ADDON_SET); + Assert.equal(addonSet, `{"schema":1,"addons":{}}`); + + // Add-on will still be present + let addon = await promiseAddonByID("system1@tests.mozilla.org"); + Assert.notEqual(addon, null); + Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_NOT_REQUIRED); + + let conditions = [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: "1.0" }, + ]; + + await check_installed(conditions); + + await promiseShutdownManager(); + + AddonTestUtils.usePrivilegedSignatures = id => "system"; +}); + +// A failed upgrade should revert to the default set. +add_task(async function test_updated_bad_update_set() { + // Create a random dir to install into + let dirname = makeUUID(); + let dir = FileUtils.getDir("ProfD", ["features", dirname]); + dir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + updatesDir.append(dirname); + + // Copy in the system add-ons + let file = await getSystemAddonXPI(2, "2.0"); + file.copyTo(updatesDir, "system2@tests.mozilla.org.xpi"); + file = await getSystemAddonXPI("failed_update", "1.0"); + file.copyTo(updatesDir, "system_failed_update@tests.mozilla.org.xpi"); + + // Inject it into the system set + let addonSet = { + schema: 1, + directory: updatesDir.leafName, + addons: { + "system2@tests.mozilla.org": { + version: "2.0", + }, + "system_failed_update@tests.mozilla.org": { + version: "1.0", + }, + }, + }; + Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, JSON.stringify(addonSet)); + + await overrideBuiltIns({ + system: [ + "system1@tests.mozilla.org", + "system2@tests.mozilla.org", + "system3@tests.mozilla.org", + "system5@tests.mozilla.org", + ], + }); + await promiseStartupManager(); + + let conditions = [{ isUpgrade: false, version: "1.0" }]; + + await check_installed(conditions); + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_blank.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_blank.js new file mode 100644 index 0000000000..56d10436c7 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_blank.js @@ -0,0 +1,118 @@ +// Tests that system add-on upgrades work. + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2"); + +let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); +distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", distroDir); + +AddonTestUtils.usePrivilegedSignatures = id => "system"; + +add_task(() => initSystemAddonDirs()); + +/** + * Defines the set of initial conditions to run each test against. Each should + * define the following properties: + * + * setup: A task to setup the profile into the initial state. + * initialState: The initial expected system add-on state after setup has run. + */ +const TEST_CONDITIONS = { + // Runs tests with no updated or default system add-ons initially installed + blank: { + setup() { + clearSystemAddonUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + // Runs tests with default system add-ons installed + withAppSet: { + setup() { + clearSystemAddonUpdatesDir(); + distroDir.leafName = "prefilled"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + + // Runs tests with updated system add-ons installed + withProfileSet: { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + + // Runs tests with both default and updated system add-ons installed + withBothSets: { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "hidden"; + }, + initialState: [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, +}; + +/** + * The tests to run. Each test must define an updateList or test. The following + * properties are used: + * + * updateList: The set of add-ons the server should respond with. + * test: A function to run to perform the update check (replaces + * updateList) + * fails: An optional property, if true the update check is expected to + * fail. + * finalState: An optional property, the expected final state of system add-ons, + * if missing the test condition's initialState is used. + */ +const TESTS = { + // Test that a blank response does nothing + blank: { + updateList: null, + }, +}; + +add_task(async function setup() { + // Initialise the profile + await overrideBuiltIns({ system: [] }); + await promiseStartupManager(); + await promiseShutdownManager(); +}); + +add_task(async function () { + for (let setupName of Object.keys(TEST_CONDITIONS)) { + for (let testName of Object.keys(TESTS)) { + info("Running test " + setupName + " " + testName); + + let setup = TEST_CONDITIONS[setupName]; + let test = TESTS[testName]; + + await execSystemAddonTest(setupName, setup, test, distroDir); + } + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_checkSizeHash.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_checkSizeHash.js new file mode 100644 index 0000000000..f9ac09255a --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_checkSizeHash.js @@ -0,0 +1,182 @@ +// Tests that system add-on upgrades work. + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2"); + +let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); +distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", distroDir); + +AddonTestUtils.usePrivilegedSignatures = id => "system"; + +/** + * Defines the set of initial conditions to run each test against. Each should + * define the following properties: + * + * setup: A task to setup the profile into the initial state. + * initialState: The initial expected system add-on state after setup has run. + */ +const TEST_CONDITIONS = { + // Runs tests with no updated or default system add-ons initially installed + blank: { + setup() { + clearSystemAddonUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + // Runs tests with default system add-ons installed + withAppSet: { + setup() { + clearSystemAddonUpdatesDir(); + distroDir.leafName = "prefilled"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + + // Runs tests with updated system add-ons installed + withProfileSet: { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + + // Runs tests with both default and updated system add-ons installed + withBothSets: { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "hidden"; + }, + initialState: [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, +}; + +/** + * The tests to run. Each test must define an updateList or test. The following + * properties are used: + * + * updateList: The set of add-ons the server should respond with. + * test: A function to run to perform the update check (replaces + * updateList) + * fails: An optional property, if true the update check is expected to + * fail. + * finalState: An optional property, the expected final state of system add-ons, + * if missing the test condition's initialState is used. + */ +const TESTS = { + // Correct sizes and hashes should work + checkSizeHash: { + // updateList is populated in setup() + updateList: [], + finalState: { + blank: [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "1.0" }, + ], + withAppSet: [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "1.0" }, + ], + withProfileSet: [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "1.0" }, + ], + withBothSets: [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "1.0" }, + ], + }, + }, +}; + +add_task(async function setup() { + await initSystemAddonDirs(); + + // Initialise the profile + await overrideBuiltIns({ system: [] }); + await promiseStartupManager(); + await promiseShutdownManager(); + + let list = TESTS.checkSizeHash.updateList; + let xpi = await getSystemAddonXPI(2, "3.0"); + list.push({ + id: "system2@tests.mozilla.org", + version: "3.0", + path: "system2_3.xpi", + xpi, + size: xpi.fileSize, + }); + + xpi = await getSystemAddonXPI(3, "3.0"); + let [hashFunction, hashValue] = do_get_file_hash(xpi, "sha256").split(":"); + list.push({ + id: "system3@tests.mozilla.org", + version: "3.0", + path: "system3_3.xpi", + xpi, + hashFunction, + hashValue, + }); + + xpi = await getSystemAddonXPI(5, "1.0"); + [hashFunction, hashValue] = do_get_file_hash(xpi, "sha256").split(":"); + list.push({ + id: "system5@tests.mozilla.org", + version: "1.0", + path: "system5_1.xpi", + size: xpi.fileSize, + xpi, + hashFunction, + hashValue, + }); +}); + +add_task(async function () { + for (let setupName of Object.keys(TEST_CONDITIONS)) { + for (let testName of Object.keys(TESTS)) { + info("Running test " + setupName + " " + testName); + + let setup = TEST_CONDITIONS[setupName]; + let test = TESTS[testName]; + + await execSystemAddonTest(setupName, setup, test, distroDir); + } + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_custom.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_custom.js new file mode 100644 index 0000000000..b0310d3ceb --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_custom.js @@ -0,0 +1,492 @@ +// Tests that system add-on upgrades work. + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2"); + +let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); +distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", distroDir); + +AddonTestUtils.usePrivilegedSignatures = id => "system"; + +add_task(initSystemAddonDirs); + +/** + * Defines the set of initial conditions to run each test against. Each should + * define the following properties: + * + * setup: A task to setup the profile into the initial state. + * initialState: The initial expected system add-on state after setup has run. + */ +const TEST_CONDITIONS = { + // Runs tests with no updated or default system add-ons initially installed + blank: { + setup() { + clearSystemAddonUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + // Runs tests with default system add-ons installed + withAppSet: { + setup() { + clearSystemAddonUpdatesDir(); + distroDir.leafName = "prefilled"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + + // Runs tests with updated system add-ons installed + withProfileSet: { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + + // Runs tests with both default and updated system add-ons installed + withBothSets: { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "hidden"; + }, + initialState: [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, +}; + +// Test that the update check is performed as part of the regular add-on update +// check +add_task(async function test_addon_update() { + Services.prefs.setBoolPref(PREF_SYSTEM_ADDON_UPDATE_ENABLED, true); + await setupSystemAddonConditions(TEST_CONDITIONS.blank, distroDir); + + let updateXML = buildSystemAddonUpdates([ + { + id: "system2@tests.mozilla.org", + version: "2.0", + path: "system2_2.xpi", + xpi: await getSystemAddonXPI(2, "2.0"), + }, + { + id: "system3@tests.mozilla.org", + version: "2.0", + path: "system3_2.xpi", + xpi: await getSystemAddonXPI(3, "2.0"), + }, + ]); + + const promiseInstallsEnded = createInstallsEndedPromise(2); + + await Promise.all([ + updateAllSystemAddons(updateXML), + promiseWebExtensionStartup("system2@tests.mozilla.org"), + promiseWebExtensionStartup("system3@tests.mozilla.org"), + ]); + + await promiseInstallsEnded; + + await verifySystemAddonState( + TEST_CONDITIONS.blank.initialState, + [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + false, + distroDir + ); + + await promiseShutdownManager(); +}); + +// Disabling app updates should block system add-on updates +add_task(async function test_app_update_disabled() { + await setupSystemAddonConditions(TEST_CONDITIONS.blank, distroDir); + + Services.prefs.setBoolPref(PREF_SYSTEM_ADDON_UPDATE_ENABLED, false); + let updateXML = buildSystemAddonUpdates([ + { + id: "system2@tests.mozilla.org", + version: "2.0", + path: "system2_2.xpi", + xpi: await getSystemAddonXPI(2, "2.0"), + }, + { + id: "system3@tests.mozilla.org", + version: "2.0", + path: "system3_2.xpi", + xpi: await getSystemAddonXPI(3, "2.0"), + }, + ]); + await updateAllSystemAddons(updateXML); + Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_UPDATE_ENABLED); + + await verifySystemAddonState( + TEST_CONDITIONS.blank.initialState, + undefined, + false, + distroDir + ); + + await promiseShutdownManager(); +}); + +// Safe mode should block system add-on updates +add_task(async function test_safe_mode() { + gAppInfo.inSafeMode = true; + + await setupSystemAddonConditions(TEST_CONDITIONS.blank, distroDir); + + Services.prefs.setBoolPref(PREF_SYSTEM_ADDON_UPDATE_ENABLED, false); + let updateXML = buildSystemAddonUpdates([ + { + id: "system2@tests.mozilla.org", + version: "2.0", + path: "system2_2.xpi", + xpi: await getSystemAddonXPI(2, "2.0"), + }, + { + id: "system3@tests.mozilla.org", + version: "2.0", + path: "system3_2.xpi", + xpi: await getSystemAddonXPI(3, "2.0"), + }, + ]); + await updateAllSystemAddons(updateXML); + Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_UPDATE_ENABLED); + + await verifySystemAddonState( + TEST_CONDITIONS.blank.initialState, + undefined, + false, + distroDir + ); + + await promiseShutdownManager(); + + gAppInfo.inSafeMode = false; +}); + +// Tests that a set that matches the default set does nothing +add_task(async function test_match_default() { + await setupSystemAddonConditions(TEST_CONDITIONS.withAppSet, distroDir); + + let installXML = buildSystemAddonUpdates([ + { + id: "system2@tests.mozilla.org", + version: "2.0", + path: "system2_2.xpi", + xpi: await getSystemAddonXPI(2, "2.0"), + }, + { + id: "system3@tests.mozilla.org", + version: "2.0", + path: "system3_2.xpi", + xpi: await getSystemAddonXPI(3, "2.0"), + }, + ]); + await installSystemAddons(installXML); + + // Shouldn't have installed an updated set + await verifySystemAddonState( + TEST_CONDITIONS.withAppSet.initialState, + undefined, + false, + distroDir + ); + + await promiseShutdownManager(); +}); + +// Tests that a set that matches the hidden default set works +add_task(async function test_match_default_revert() { + await setupSystemAddonConditions(TEST_CONDITIONS.withBothSets, distroDir); + + let installXML = buildSystemAddonUpdates([ + { + id: "system1@tests.mozilla.org", + version: "1.0", + path: "system1_1.xpi", + xpi: await getSystemAddonXPI(1, "1.0"), + }, + { + id: "system2@tests.mozilla.org", + version: "1.0", + path: "system2_1.xpi", + xpi: await getSystemAddonXPI(2, "1.0"), + }, + ]); + await installSystemAddons(installXML); + + // This should revert to the default set instead of installing new versions + // into an updated set. + await verifySystemAddonState( + TEST_CONDITIONS.withBothSets.initialState, + [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + false, + distroDir + ); + + await promiseShutdownManager(); +}); + +// Tests that a set that matches the current set works +add_task(async function test_match_current() { + await setupSystemAddonConditions(TEST_CONDITIONS.withBothSets, distroDir); + + let installXML = buildSystemAddonUpdates([ + { + id: "system2@tests.mozilla.org", + version: "2.0", + path: "system2_2.xpi", + xpi: await getSystemAddonXPI(2, "2.0"), + }, + { + id: "system3@tests.mozilla.org", + version: "2.0", + path: "system3_2.xpi", + xpi: await getSystemAddonXPI(3, "2.0"), + }, + ]); + await installSystemAddons(installXML); + + // This should remain with the current set instead of creating a new copy + let set = JSON.parse(Services.prefs.getCharPref(PREF_SYSTEM_ADDON_SET)); + Assert.equal(set.directory, "prefilled"); + + await verifySystemAddonState( + TEST_CONDITIONS.withBothSets.initialState, + undefined, + false, + distroDir + ); + + await promiseShutdownManager(); +}); + +// Tests that a set with a minor change doesn't re-download existing files +add_task(async function test_no_download() { + await setupSystemAddonConditions(TEST_CONDITIONS.withBothSets, distroDir); + + // The missing file here is unneeded since there is a local version already + let installXML = buildSystemAddonUpdates([ + { id: "system2@tests.mozilla.org", version: "2.0", path: "missing.xpi" }, + { + id: "system4@tests.mozilla.org", + version: "1.0", + path: "system4_1.xpi", + xpi: await getSystemAddonXPI(4, "1.0"), + }, + ]); + + const promiseInstallsEnded = createInstallsEndedPromise(2); + + await Promise.all([ + installSystemAddons(installXML), + promiseWebExtensionStartup("system4@tests.mozilla.org"), + ]); + + // NOTE: verifySystemAddonState does call AddonTestUtils.promiseShutdownManager + // internally, and so we need to wait for the system addons to be fully + // installed and the system addon location's stating dir to have been released. + info("Wait for system addon installs to be completed"); + await promiseInstallsEnded; + info("Wait for system addons stating dir to be released"); + await waitForSystemAddonStagingDirReleased(); + + await verifySystemAddonState( + TEST_CONDITIONS.withBothSets.initialState, + [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "1.0" }, + { isUpgrade: false, version: null }, + ], + false, + distroDir + ); + + await promiseShutdownManager(); +}); + +// Tests that a second update before a restart works +add_task(async function test_double_update() { + await setupSystemAddonConditions(TEST_CONDITIONS.withAppSet, distroDir); + + let installXML = buildSystemAddonUpdates([ + { + id: "system2@tests.mozilla.org", + version: "2.0", + path: "system2_2.xpi", + xpi: await getSystemAddonXPI(2, "2.0"), + }, + { + id: "system3@tests.mozilla.org", + version: "1.0", + path: "system3_1.xpi", + xpi: await getSystemAddonXPI(3, "1.0"), + }, + ]); + await Promise.all([ + installSystemAddons(installXML), + promiseWebExtensionStartup("system2@tests.mozilla.org"), + promiseWebExtensionStartup("system3@tests.mozilla.org"), + ]); + + installXML = buildSystemAddonUpdates([ + { + id: "system3@tests.mozilla.org", + version: "2.0", + path: "system3_2.xpi", + xpi: await getSystemAddonXPI(3, "2.0"), + }, + { + id: "system4@tests.mozilla.org", + version: "1.0", + path: "system4_1.xpi", + xpi: await getSystemAddonXPI(4, "1.0"), + }, + ]); + await Promise.all([ + installSystemAddons(installXML), + promiseWebExtensionStartup("system3@tests.mozilla.org"), + promiseWebExtensionStartup("system4@tests.mozilla.org"), + ]); + + await verifySystemAddonState( + TEST_CONDITIONS.withAppSet.initialState, + [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "1.0" }, + { isUpgrade: false, version: null }, + ], + true, + distroDir + ); + + await promiseShutdownManager(); +}); + +// A second update after a restart will delete the original unused set +add_task(async function test_update_purges() { + await setupSystemAddonConditions(TEST_CONDITIONS.withBothSets, distroDir); + + let installXML = buildSystemAddonUpdates([ + { + id: "system2@tests.mozilla.org", + version: "2.0", + path: "system2_2.xpi", + xpi: await getSystemAddonXPI(2, "2.0"), + }, + { + id: "system3@tests.mozilla.org", + version: "1.0", + path: "system3_1.xpi", + xpi: await getSystemAddonXPI(3, "1.0"), + }, + ]); + await Promise.all([ + installSystemAddons(installXML), + promiseWebExtensionStartup("system2@tests.mozilla.org"), + promiseWebExtensionStartup("system3@tests.mozilla.org"), + ]); + + await verifySystemAddonState( + TEST_CONDITIONS.withBothSets.initialState, + [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "1.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + false, + distroDir + ); + + await installSystemAddons(buildSystemAddonUpdates(null)); + + let dirs = await getSystemAddonDirectories(); + Assert.equal(dirs.length, 1); + + await promiseShutdownManager(); +}); + +function createInstallsEndedPromise(expectedCount) { + // Addon installation triggers the execution of an un-awaited promise. We need + // to keep track of addon installs so that we can tell when these async + // processes have finished. + + return new Promise(resolve => { + let installsEnded = 0; + + const listener = { + onInstallStarted() {}, + onInstallEnded() { + installsEnded++; + + if (installsEnded === expectedCount) { + AddonManager.removeInstallListener(listener); + resolve(); + } + }, + onInstallCancelled() {}, + onInstallFailed() {}, + }; + + AddonManager.addInstallListener(listener); + }); +} + +async function waitForSystemAddonStagingDirReleased() { + // Wait for the staging dir to be released, which prevents unexpected test + // failure due to AddonTestUtils.promiseShutdownManager being mocking an + // AddonManager shutdown by using testing functions to re-import XPIProvider, + // XPIDatabase, and XPIInstall modules, which would hit unexpected failures + // if done while system addon updates are still running in the background. + + const { XPIExports } = ChromeUtils.importESModule( + "resource://gre/modules/addons/XPIExports.sys.mjs" + ); + let systemAddonLocation = XPIExports.XPIInternal.XPIStates.getLocation( + XPIExports.XPIInternal.KEY_APP_SYSTEM_ADDONS + ); + await TestUtils.waitForCondition(() => { + return systemAddonLocation.installer._stagingDirLock == 0; + }, "Wait for staging dir to be released"); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_empty.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_empty.js new file mode 100644 index 0000000000..3fae4272be --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_empty.js @@ -0,0 +1,142 @@ +// Tests that system add-on upgrades work. + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2"); + +let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); +distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", distroDir); + +AddonTestUtils.usePrivilegedSignatures = id => "system"; + +add_task(() => initSystemAddonDirs()); + +/** + * Defines the set of initial conditions to run each test against. Each should + * define the following properties: + * + * setup: A task to setup the profile into the initial state. + * initialState: The initial expected system add-on state after setup has run. + */ +const TEST_CONDITIONS = { + // Runs tests with no updated or default system add-ons initially installed + blank: { + setup() { + clearSystemAddonUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + // Runs tests with default system add-ons installed + withAppSet: { + setup() { + clearSystemAddonUpdatesDir(); + distroDir.leafName = "prefilled"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + + // Runs tests with updated system add-ons installed + withProfileSet: { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + + // Runs tests with both default and updated system add-ons installed + withBothSets: { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "hidden"; + }, + initialState: [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, +}; + +/** + * The tests to run. Each test must define an updateList or test. The following + * properties are used: + * + * updateList: The set of add-ons the server should respond with. + * test: A function to run to perform the update check (replaces + * updateList) + * fails: An optional property, if true the update check is expected to + * fail. + * finalState: An optional property, the expected final state of system add-ons, + * if missing the test condition's initialState is used. + */ +const TESTS = { + // Test that an empty list removes existing updates, leaving defaults. + empty: { + updateList: [], + finalState: { + blank: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + withAppSet: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + withProfileSet: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + withBothSets: [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + // Set this to `true` to so `verifySystemAddonState()` expects a blank profile dir + { isUpgrade: true, version: null }, + ], + }, + }, +}; + +add_task(async function () { + for (let setupName of Object.keys(TEST_CONDITIONS)) { + for (let testName of Object.keys(TESTS)) { + info("Running test " + setupName + " " + testName); + + let setup = TEST_CONDITIONS[setupName]; + let test = TESTS[testName]; + + await execSystemAddonTest(setupName, setup, test, distroDir); + } + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_enterprisepolicy.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_enterprisepolicy.js new file mode 100644 index 0000000000..d36b97a3cc --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_enterprisepolicy.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// This test verifies that system addon updates are correctly blocked by the +// DisableSystemAddonUpdate enterprise policy. + +const { EnterprisePolicyTesting } = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" +); + +Services.policies; // Load policy engine + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2"); + +let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); +distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", distroDir); +add_task(() => initSystemAddonDirs()); + +/** + * Defines the set of initial conditions to run the test against. + * + * setup: A task to setup the profile into the initial state. + * initialState: The initial expected system add-on state after setup has run. + * + * These conditions run tests with no updated or default system add-ons + * initially installed + */ +const TEST_CONDITIONS = { + setup() { + clearSystemAddonUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], +}; + +add_task(async function test_update_disabled_by_policy() { + await setupSystemAddonConditions(TEST_CONDITIONS, distroDir); + + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + DisableSystemAddonUpdate: true, + }, + }); + + await updateAllSystemAddons( + buildSystemAddonUpdates([ + { + id: "system2@tests.mozilla.org", + version: "2.0", + path: "system2_2.xpi", + xpi: await getSystemAddonXPI(2, "2.0"), + }, + { + id: "system3@tests.mozilla.org", + version: "2.0", + path: "system3_2.xpi", + xpi: await getSystemAddonXPI(3, "2.0"), + }, + ]) + ); + + await verifySystemAddonState( + TEST_CONDITIONS.initialState, + undefined, + false, + distroDir + ); + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_fail.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_fail.js new file mode 100644 index 0000000000..377c30395a --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_fail.js @@ -0,0 +1,186 @@ +// Tests that system add-on upgrades fail to upgrade in expected cases. + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2"); + +let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); +distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", distroDir); +add_task(() => initSystemAddonDirs()); + +/** + * Defines the set of initial conditions to run each test against. Each should + * define the following properties: + * + * setup: A task to setup the profile into the initial state. + * initialState: The initial expected system add-on state after setup has run. + */ +const TEST_CONDITIONS = { + // Runs tests with no updated or default system add-ons initially installed + blank: { + setup() { + clearSystemAddonUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + // Runs tests with default system add-ons installed + withAppSet: { + setup() { + clearSystemAddonUpdatesDir(); + distroDir.leafName = "prefilled"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + + // Runs tests with updated system add-ons installed + withProfileSet: { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + + // Runs tests with both default and updated system add-ons installed + withBothSets: { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "hidden"; + }, + initialState: [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, +}; + +/** + * The tests to run. Each test must define an updateList or test. The following + * properties are used: + * + * updateList: The set of add-ons the server should respond with. + * test: A function to run to perform the update check (replaces + * updateList) + * fails: regex to test error in Assert.rejects. + * finalState: An optional property, the expected final state of system add-ons, + * if missing the test condition's initialState is used. + */ +const TESTS = { + // Specifying an incorrect version should stop us updating anything + badVersion: { + fails: + /Error: Rejecting updated system add-on set that either could not be downloaded or contained unusable add-ons./, + updateList: [ + { + id: "system2@tests.mozilla.org", + version: "4.0", + path: "system2_3.xpi", + }, + { + id: "system3@tests.mozilla.org", + version: "3.0", + path: "system3_3.xpi", + }, + ], + }, + + // Specifying an invalid size should stop us updating anything + badSize: { + fails: + /Error: Rejecting updated system add-on set that either could not be downloaded or contained unusable add-ons./, + updateList: [ + { + id: "system2@tests.mozilla.org", + version: "3.0", + path: "system2_3.xpi", + size: 2, + }, + { + id: "system3@tests.mozilla.org", + version: "3.0", + path: "system3_3.xpi", + }, + ], + }, + + // Specifying an incorrect hash should stop us updating anything + badHash: { + fails: + /Error: Rejecting updated system add-on set that either could not be downloaded or contained unusable add-ons./, + updateList: [ + { + id: "system2@tests.mozilla.org", + version: "3.0", + path: "system2_3.xpi", + }, + { + id: "system3@tests.mozilla.org", + version: "3.0", + path: "system3_3.xpi", + hashFunction: "sha256", + hashValue: "205a4c49bd513ebd30594e380c19e86bba1f83e2", + }, + ], + }, + + // A bad certificate should stop updates + badCert: { + fails: + /Error: Rejecting updated system add-on set that either could not be downloaded or contained unusable add-ons./, + // true is not system addon signed + usePrivilegedSignatures: true, + updateList: [ + { + id: "system1@tests.mozilla.org", + version: "1.0", + path: "system1_1_badcert.xpi", + }, + { + id: "system3@tests.mozilla.org", + version: "1.0", + path: "system3_1.xpi", + }, + ], + }, +}; + +add_task(async function setup() { + // Initialise the profile + await overrideBuiltIns({ system: [] }); + await promiseStartupManager(); + await promiseShutdownManager(); +}); + +add_task(async function () { + for (let setupName of Object.keys(TEST_CONDITIONS)) { + for (let testName of Object.keys(TESTS)) { + info("Running test " + setupName + " " + testName); + + let setup = TEST_CONDITIONS[setupName]; + let test = TESTS[testName]; + + await execSystemAddonTest(setupName, setup, test, distroDir); + } + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_installTelemetryInfo.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_installTelemetryInfo.js new file mode 100644 index 0000000000..f67894289d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_installTelemetryInfo.js @@ -0,0 +1,95 @@ +// Test that the expected installTelemetryInfo are being set on the system addon +// installed/updated through Balrog. + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2"); + +let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); +distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", distroDir); + +AddonTestUtils.usePrivilegedSignatures = id => "system"; + +add_task(() => initSystemAddonDirs()); + +add_task(async function test_addon_update() { + Services.prefs.setBoolPref(PREF_SYSTEM_ADDON_UPDATE_ENABLED, true); + + // Define the set of initial conditions to run each test against: + // - setup: A task to setup the profile into the initial state. + // - initialState: The initial expected system add-on state after setup has run. + const initialSetup = { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "2.0" }, + ], + }; + + await setupSystemAddonConditions(initialSetup, distroDir); + + const newlyInstalledSystemAddonId = "system1@tests.mozilla.org"; + const updatedSystemAddonId = "system2@tests.mozilla.org"; + + const updateXML = buildSystemAddonUpdates([ + // Newly installed system addon entry. + { + id: newlyInstalledSystemAddonId, + version: "1.0", + path: "system1_1.xpi", + xpi: await getSystemAddonXPI(1, "1.0"), + }, + // Updated system addon entry. + { + id: updatedSystemAddonId, + version: "3.0", + path: "system2_3.xpi", + xpi: await getSystemAddonXPI(2, "3.0"), + }, + ]); + + await Promise.all([ + updateAllSystemAddons(updateXML), + promiseWebExtensionStartup(newlyInstalledSystemAddonId), + promiseWebExtensionStartup(updatedSystemAddonId), + ]); + + await verifySystemAddonState( + initialSetup.initialState, + [ + { isUpgrade: true, version: "1.0" }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + false, + distroDir + ); + + const newlyInstalledSystemAddon = await AddonManager.getAddonByID( + newlyInstalledSystemAddonId + ); + Assert.deepEqual( + newlyInstalledSystemAddon.installTelemetryInfo, + // For addons installed for the first time through the product addon checker + // we set a `method` in the telemetryInfo. + { source: "system-addon", method: "product-updates" }, + "Got the expected telemetry info on balrog system addon installed addon" + ); + + const updatedSystemAddon = await AddonManager.getAddonByID( + updatedSystemAddonId + ); + Assert.deepEqual( + updatedSystemAddon.installTelemetryInfo, + // For addons that are distributed in Firefox, then updated through the product + // addon checker, `method` will not be set. + { source: "system-addon" }, + "Got the expected telemetry info on balrog system addon updated addon" + ); + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_newset.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_newset.js new file mode 100644 index 0000000000..fd93ba5d38 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_newset.js @@ -0,0 +1,166 @@ +// Tests that system add-on upgrades work. + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2"); + +let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); +distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", distroDir); + +AddonTestUtils.usePrivilegedSignatures = id => "system"; + +add_task(() => initSystemAddonDirs()); + +/** + * Defines the set of initial conditions to run each test against. Each should + * define the following properties: + * + * setup: A task to setup the profile into the initial state. + * initialState: The initial expected system add-on state after setup has run. + */ +const TEST_CONDITIONS = { + // Runs tests with no updated or default system add-ons initially installed + blank: { + setup() { + clearSystemAddonUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + // Runs tests with default system add-ons installed + withAppSet: { + setup() { + clearSystemAddonUpdatesDir(); + distroDir.leafName = "prefilled"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + + // Runs tests with updated system add-ons installed + withProfileSet: { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + + // Runs tests with both default and updated system add-ons installed + withBothSets: { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "hidden"; + }, + initialState: [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, +}; + +/** + * The tests to run. Each test must define an updateList or test. The following + * properties are used: + * + * updateList: The set of add-ons the server should respond with. + * test: A function to run to perform the update check (replaces + * updateList) + * fails: An optional property, if true the update check is expected to + * fail. + * finalState: An optional property, the expected final state of system add-ons, + * if missing the test condition's initialState is used. + */ +const TESTS = { + // Tests that a new set of system add-ons gets installed + newset: { + // updateList is populated in setup() below + updateList: [], + finalState: { + blank: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "1.0" }, + { isUpgrade: true, version: "1.0" }, + ], + withAppSet: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: true, version: "1.0" }, + { isUpgrade: true, version: "1.0" }, + ], + withProfileSet: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "1.0" }, + { isUpgrade: true, version: "1.0" }, + ], + withBothSets: [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: "1.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "1.0" }, + { isUpgrade: true, version: "1.0" }, + ], + }, + }, +}; + +add_task(async function setup() { + // Initialise the profile + await overrideBuiltIns({ system: [] }); + await promiseStartupManager(); + await promiseShutdownManager(); + + let list = TESTS.newset.updateList; + let xpi = await getSystemAddonXPI(4, "1.0"); + list.push({ + id: "system4@tests.mozilla.org", + version: "1.0", + path: "system4_1.xpi", + xpi, + }); + + xpi = await getSystemAddonXPI(5, "1.0"); + list.push({ + id: "system5@tests.mozilla.org", + version: "1.0", + path: "system5_1.xpi", + xpi, + }); +}); + +add_task(async function () { + for (let setupName of Object.keys(TEST_CONDITIONS)) { + for (let testName of Object.keys(TESTS)) { + info("Running test " + setupName + " " + testName); + + let setup = TEST_CONDITIONS[setupName]; + let test = TESTS[testName]; + + await execSystemAddonTest(setupName, setup, test, distroDir); + } + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_overlapping.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_overlapping.js new file mode 100644 index 0000000000..a6c5bc905c --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_overlapping.js @@ -0,0 +1,181 @@ +// Tests that system add-on upgrades work. + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2"); + +let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); +distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", distroDir); +add_task(() => initSystemAddonDirs()); + +AddonTestUtils.usePrivilegedSignatures = id => "system"; + +/** + * Defines the set of initial conditions to run each test against. Each should + * define the following properties: + * + * setup: A task to setup the profile into the initial state. + * initialState: The initial expected system add-on state after setup has run. + */ +const TEST_CONDITIONS = { + // Runs tests with no updated or default system add-ons initially installed + blank: { + setup() { + clearSystemAddonUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + // Runs tests with default system add-ons installed + withAppSet: { + setup() { + clearSystemAddonUpdatesDir(); + distroDir.leafName = "prefilled"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + + // Runs tests with updated system add-ons installed + withProfileSet: { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + + // Runs tests with both default and updated system add-ons installed + withBothSets: { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "hidden"; + }, + initialState: [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, +}; + +/** + * The tests to run. Each test must define an updateList or test. The following + * properties are used: + * + * updateList: The set of add-ons the server should respond with. + * test: A function to run to perform the update check (replaces + * updateList) + * fails: An optional property, if true the update check is expected to + * fail. + * finalState: An optional property, the expected final state of system add-ons, + * if missing the test condition's initialState is used. + */ +const TESTS = { + // Tests that a set of system add-ons, some new, some existing gets installed + overlapping: { + // updateList is populated in setup() below + updateList: [], + finalState: { + blank: [ + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: true, version: "1.0" }, + { isUpgrade: false, version: null }, + ], + withAppSet: [ + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: true, version: "1.0" }, + { isUpgrade: false, version: null }, + ], + withProfileSet: [ + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: true, version: "1.0" }, + { isUpgrade: false, version: null }, + ], + withBothSets: [ + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: true, version: "1.0" }, + { isUpgrade: false, version: null }, + ], + }, + }, +}; + +add_task(async function setup() { + // Initialise the profile + await overrideBuiltIns({ system: [] }); + await promiseStartupManager(); + await promiseShutdownManager(); + + let list = TESTS.overlapping.updateList; + let xpi = await getSystemAddonXPI(1, "2.0"); + list.push({ + id: "system1@tests.mozilla.org", + version: "2.0", + path: "system1_2.xpi", + xpi, + }); + + xpi = await getSystemAddonXPI(2, "2.0"); + list.push({ + id: "system2@tests.mozilla.org", + version: "2.0", + path: "system2_2.xpi", + xpi, + }); + + xpi = await getSystemAddonXPI(3, "3.0"); + list.push({ + id: "system3@tests.mozilla.org", + version: "3.0", + path: "system3_3.xpi", + xpi, + }); + + xpi = await getSystemAddonXPI(4, "1.0"); + list.push({ + id: "system4@tests.mozilla.org", + version: "1.0", + path: "system4_1.xpi", + xpi, + }); +}); + +add_task(async function () { + for (let setupName of Object.keys(TEST_CONDITIONS)) { + for (let testName of Object.keys(TESTS)) { + info("Running test " + setupName + " " + testName); + + let setup = TEST_CONDITIONS[setupName]; + let test = TESTS[testName]; + + await execSystemAddonTest(setupName, setup, test, distroDir); + } + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_uninstall_check.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_uninstall_check.js new file mode 100644 index 0000000000..bf2dd85772 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_uninstall_check.js @@ -0,0 +1,57 @@ +// Tests that system add-on doesnt uninstall while update. + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2"); + +let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); +distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", distroDir); + +AddonTestUtils.usePrivilegedSignatures = "system"; + +add_task(() => initSystemAddonDirs()); + +const initialSetup = { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "2.0" }, + ], +}; + +add_task(async function test_systems_update_uninstall_check() { + Services.prefs.setBoolPref(PREF_SYSTEM_ADDON_UPDATE_ENABLED, true); + + await setupSystemAddonConditions(initialSetup, distroDir); + + let updateXML = buildSystemAddonUpdates([ + { + id: "system2@tests.mozilla.org", + version: "3.0", + path: "system2_3.xpi", + xpi: await getSystemAddonXPI(2, "3.0"), + }, + ]); + + const listener = (msg, { method, params, reason }) => { + if (params.id === "system2@tests.mozilla.org" && method === "uninstall") { + Assert.ok( + false, + "Should not see uninstall call for system2@tests.mozilla.org" + ); + } + }; + + AddonTestUtils.on("bootstrap-method", listener); + + await Promise.all([ + updateAllSystemAddons(updateXML), + promiseWebExtensionStartup("system2@tests.mozilla.org"), + ]); + + AddonTestUtils.off("bootstrap-method", listener); + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_upgrades.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_upgrades.js new file mode 100644 index 0000000000..d270f33190 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_upgrades.js @@ -0,0 +1,166 @@ +// Tests that system add-on upgrades work. + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2"); + +let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); +distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", distroDir); + +AddonTestUtils.usePrivilegedSignatures = id => "system"; + +add_task(() => initSystemAddonDirs()); + +/** + * Defines the set of initial conditions to run each test against. Each should + * define the following properties: + * + * setup: A task to setup the profile into the initial state. + * initialState: The initial expected system add-on state after setup has run. + */ +const TEST_CONDITIONS = { + // Runs tests with no updated or default system add-ons initially installed + blank: { + setup() { + clearSystemAddonUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + // Runs tests with default system add-ons installed + withAppSet: { + setup() { + clearSystemAddonUpdatesDir(); + distroDir.leafName = "prefilled"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + + // Runs tests with updated system add-ons installed + withProfileSet: { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + + // Runs tests with both default and updated system add-ons installed + withBothSets: { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "hidden"; + }, + initialState: [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: true, version: "2.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, +}; + +/** + * The tests to run. Each test must define an updateList or test. The following + * properties are used: + * + * updateList: The set of add-ons the server should respond with. + * test: A function to run to perform the update check (replaces + * updateList) + * fails: An optional property, if true the update check is expected to + * fail. + * finalState: An optional property, the expected final state of system add-ons, + * if missing the test condition's initialState is used. + */ +const TESTS = { + // Tests that an upgraded set of system add-ons gets installed + upgrades: { + // populated in setup() below + updateList: [], + finalState: { + blank: [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + withAppSet: [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + withProfileSet: [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + withBothSets: [ + { isUpgrade: false, version: "1.0" }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: true, version: "3.0" }, + { isUpgrade: false, version: null }, + { isUpgrade: false, version: null }, + ], + }, + }, +}; + +add_task(async function setup() { + // Initialise the profile + await overrideBuiltIns({ system: [] }); + await promiseStartupManager(); + await promiseShutdownManager(); + + let list = TESTS.upgrades.updateList; + let xpi = await getSystemAddonXPI(2, "3.0"); + list.push({ + id: "system2@tests.mozilla.org", + version: "3.0", + path: "system2_3.xpi", + xpi, + }); + + xpi = await getSystemAddonXPI(3, "3.0"); + list.push({ + id: "system3@tests.mozilla.org", + version: "3.0", + path: "system3_3.xpi", + xpi, + }); +}); + +add_task(async function () { + for (let setupName of Object.keys(TEST_CONDITIONS)) { + for (let testName of Object.keys(TESTS)) { + info("Running test " + setupName + " " + testName); + + let setup = TEST_CONDITIONS[setupName]; + let test = TESTS[testName]; + + await execSystemAddonTest(setupName, setup, test, distroDir); + } + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_upgrades.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_upgrades.js new file mode 100644 index 0000000000..f02003805c --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_upgrades.js @@ -0,0 +1,417 @@ +"use strict"; + +// Enable SCOPE_APPLICATION for builtin testing. Default in tests is only SCOPE_PROFILE. +let scopes = AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION; +Services.prefs.setIntPref("extensions.enabledScopes", scopes); + +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); +BootstrapMonitor.init(); + +// A test directory for default/builtin system addons. +const systemDefaults = FileUtils.getDir("ProfD", [ + "app-system-defaults", + "features", +]); +systemDefaults.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", systemDefaults); + +AddonTestUtils.usePrivilegedSignatures = id => "system"; + +const ADDON_ID = "updates@test"; + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false); + +let server = createHttpServer(); + +server.registerPathHandler("/upgrade.json", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "ok"); + response.write( + JSON.stringify({ + addons: { + [ADDON_ID]: { + updates: [ + { + version: "4.0", + update_link: `http://localhost:${server.identity.primaryPort}/${ADDON_ID}.xpi`, + }, + ], + }, + }, + }) + ); +}); + +function createWebExtensionFile(id, version, update_url) { + return AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version, + browser_specific_settings: { + gecko: { id, update_url }, + }, + }, + }); +} + +let xpiUpdate = createWebExtensionFile(ADDON_ID, "4.0"); + +server.registerFile(`/${ADDON_ID}.xpi`, xpiUpdate); + +async function promiseInstallDefaultSystemAddon(id, version) { + let xpi = createWebExtensionFile(id, version); + await AddonTestUtils.manuallyInstall(xpi, systemDefaults); + return xpi; +} + +async function promiseInstallProfileExtension(id, version, update_url) { + return promiseInstallWebExtension({ + manifest: { + version, + browser_specific_settings: { + gecko: { id, update_url }, + }, + }, + }); +} + +async function promiseInstallSystemProfileAddon(id, version) { + let xpi = createWebExtensionFile(id, version); + const install = await AddonManager.getInstallForURL(`file://${xpi.path}`, { + useSystemLocation: true, // KEY_APP_SYSTEM_PROFILE + }); + + return install.install(); +} + +async function promiseUpdateSystemAddon(id, version, waitForStartup = true) { + let xpi = createWebExtensionFile(id, version); + let xml = buildSystemAddonUpdates([ + { + id: ADDON_ID, + version, + path: xpi.leafName, + xpi, + }, + ]); + // If we're not expecting a startup we need to wait for install to end. + let promises = []; + if (!waitForStartup) { + promises.push(AddonTestUtils.promiseAddonEvent("onInstalled")); + } + promises.push(installSystemAddons(xml, waitForStartup ? [ADDON_ID] : [])); + return Promise.all(promises); +} + +async function promiseClearSystemAddons() { + let xml = buildSystemAddonUpdates([]); + return installSystemAddons(xml, []); +} + +const builtInOverride = { system: [ADDON_ID] }; + +async function checkAddon(version, reason, startReason = reason) { + let addons = await AddonManager.getAddonsByTypes(["extension"]); + Assert.equal(addons.length, 1, "one addon expected to be installed"); + Assert.equal(addons[0].version, version, `addon ${version} is installed`); + Assert.ok(addons[0].isActive, `addon ${version} is active`); + Assert.ok(!addons[0].disabled, `addon ${version} is enabled`); + + let installInfo = BootstrapMonitor.checkInstalled(ADDON_ID, version); + equal( + installInfo.reason, + reason, + `bootstrap monitor verified install reason for ${version}` + ); + let started = BootstrapMonitor.checkStarted(ADDON_ID, version); + equal( + started.reason, + startReason, + `bootstrap monitor verified started reason for ${version}` + ); + + return addons[0]; +} + +/** + * This test function starts after a 1.0 version of an addon is installed + * either as a builtin ("app-builtin") or as a builtin system addon ("app-system-defaults"). + * + * This tests the precedence chain works as expected through upgrades and + * downgrades. + * + * Upgrade to a system addon in the profile location, "app-system-addons" + * Upgrade to a temporary addon + * Uninstalling the temporary addon downgrades to the system addon in "app-system-addons". + * Upgrade to a system addon in the "app-system-profile" location. + * Uninstalling the "app-system-profile" addon downgrades to the one in "app-system-addons". + * Upgrade to a user-installed addon + * Upgrade the addon in "app-system-addons", verify user-install is still active + * Uninstall the addon in "app-system-addons", verify user-install is active + * Test that the update_url upgrades the user-install and becomes active + * Disable the user-install, verify the disabled addon retains precedence + * Uninstall the disabled user-install, verify system addon in "app-system-defaults" is active and enabled + * Upgrade the system addon again, then user-install a lower version, verify downgrade happens. + * Uninstall user-install, verify upgrade happens when the system addon in "app-system-addons" is activated. + */ +async function _test_builtin_addon_override() { + ///// + // Upgrade to a system addon in the profile location, "app-system-addons" + ///// + await promiseUpdateSystemAddon(ADDON_ID, "2.0"); + await checkAddon("2.0", BOOTSTRAP_REASONS.ADDON_UPGRADE); + + ///// + // Upgrade to a temporary addon + ///// + let tmpAddon = createWebExtensionFile(ADDON_ID, "2.1"); + await Promise.all([ + AddonManager.installTemporaryAddon(tmpAddon), + AddonTestUtils.promiseWebExtensionStartup(ADDON_ID), + ]); + let addon = await checkAddon("2.1", BOOTSTRAP_REASONS.ADDON_UPGRADE); + + ///// + // Downgrade back to the system addon + ///// + await addon.uninstall(); + await checkAddon("2.0", BOOTSTRAP_REASONS.ADDON_DOWNGRADE); + + ///// + // Install then uninstall an system profile addon + ///// + info("Install an System Profile Addon, then uninstall it."); + await Promise.all([ + promiseInstallSystemProfileAddon(ADDON_ID, "2.2"), + AddonTestUtils.promiseWebExtensionStartup(ADDON_ID), + ]); + addon = await checkAddon("2.2", BOOTSTRAP_REASONS.ADDON_UPGRADE); + await addon.uninstall(); + await checkAddon("2.0", BOOTSTRAP_REASONS.ADDON_DOWNGRADE); + + ///// + // Upgrade to a user-installed addon + ///// + await Promise.all([ + promiseInstallProfileExtension( + ADDON_ID, + "3.0", + `http://localhost:${server.identity.primaryPort}/upgrade.json` + ), + AddonTestUtils.promiseWebExtensionStartup(ADDON_ID), + ]); + await checkAddon("3.0", BOOTSTRAP_REASONS.ADDON_UPGRADE); + + ///// + // Upgrade the system addon, verify user-install is still active + ///// + await promiseUpdateSystemAddon(ADDON_ID, "2.2", false); + await checkAddon("3.0", BOOTSTRAP_REASONS.ADDON_UPGRADE); + + ///// + // Uninstall the system addon, verify user-install is active + ///// + await Promise.all([ + promiseClearSystemAddons(), + AddonTestUtils.promiseAddonEvent("onUninstalled"), + ]); + addon = await checkAddon("3.0", BOOTSTRAP_REASONS.ADDON_UPGRADE); + + ///// + // Test that the update_url upgrades the user-install and becomes active + ///// + let update = await promiseFindAddonUpdates(addon); + await Promise.all([ + promiseCompleteAllInstalls([update.updateAvailable]), + AddonTestUtils.promiseWebExtensionStartup(ADDON_ID), + ]); + addon = await checkAddon("4.0", BOOTSTRAP_REASONS.ADDON_UPGRADE); + + ///// + // Disable the user-install, verify the disabled addon retains precedence + ///// + await addon.disable(); + + await AddonManager.getAddonByID(ADDON_ID); + Assert.ok(!addon.isActive, "4.0 is disabled"); + Assert.equal( + addon.version, + "4.0", + "version 4.0 is still the visible version" + ); + + ///// + // Uninstall the disabled user-install, verify system addon is active and enabled + ///// + await Promise.all([ + addon.uninstall(), + AddonTestUtils.promiseWebExtensionStartup(ADDON_ID), + ]); + // We've downgraded all the way to 1.0 from a user-installed addon + addon = await checkAddon("1.0", BOOTSTRAP_REASONS.ADDON_DOWNGRADE); + + ///// + // Upgrade the system addon again, then user-install a lower version, verify downgrade happens. + ///// + await promiseUpdateSystemAddon(ADDON_ID, "5.1"); + addon = await checkAddon("5.1", BOOTSTRAP_REASONS.ADDON_UPGRADE); + + // user-install a lower version, downgrade happens + await Promise.all([ + promiseInstallProfileExtension(ADDON_ID, "5.0"), + AddonTestUtils.promiseWebExtensionStartup(ADDON_ID), + ]); + addon = await checkAddon("5.0", BOOTSTRAP_REASONS.ADDON_DOWNGRADE); + + ///// + // Uninstall user-install, verify upgrade happens when system addon is activated. + ///// + await Promise.all([ + addon.uninstall(), + AddonTestUtils.promiseWebExtensionStartup(ADDON_ID), + ]); + // the "system add-on upgrade" is now revealed + addon = await checkAddon("5.1", BOOTSTRAP_REASONS.ADDON_UPGRADE); + + await Promise.all([ + addon.uninstall(), + AddonTestUtils.promiseWebExtensionStartup(ADDON_ID), + ]); + await checkAddon("1.0", BOOTSTRAP_REASONS.ADDON_DOWNGRADE); + + // Downgrading from an installed system addon to a default system + // addon also requires the removal of the file on disk, and removing + // the addon from the pref. + Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_SET); +} + +add_task(async function test_system_addon_upgrades() { + await AddonTestUtils.overrideBuiltIns(builtInOverride); + await promiseInstallDefaultSystemAddon(ADDON_ID, "1.0"); + await AddonTestUtils.promiseStartupManager(); + await checkAddon("1.0", BOOTSTRAP_REASONS.ADDON_INSTALL); + + await _test_builtin_addon_override(); + + // cleanup the system addon in the default location + await AddonTestUtils.manuallyUninstall(systemDefaults, ADDON_ID); + // If we don't restart (to clean up the uninstall above) and we + // clear BootstrapMonitor, BootstrapMonitor asserts on the next AOM startup. + await AddonTestUtils.promiseRestartManager(); + + await AddonTestUtils.promiseShutdownManager(); + BootstrapMonitor.clear(ADDON_ID); +}); + +// Run the test again, but starting from a "builtin" addon location +add_task(async function test_builtin_addon_upgrades() { + builtInOverride.system = []; + + await AddonTestUtils.promiseStartupManager(); + await Promise.all([ + installBuiltinExtension({ + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { id: ADDON_ID }, + }, + }, + }), + AddonTestUtils.promiseWebExtensionStartup(ADDON_ID), + ]); + await checkAddon("1.0", BOOTSTRAP_REASONS.ADDON_INSTALL); + + await _test_builtin_addon_override(); + + let addon = await AddonManager.getAddonByID(ADDON_ID); + await addon.uninstall(); + await AddonTestUtils.promiseShutdownManager(); + BootstrapMonitor.clear(ADDON_ID); +}); + +add_task(async function test_system_addon_precedence() { + builtInOverride.system = [ADDON_ID]; + await AddonTestUtils.overrideBuiltIns(builtInOverride); + await promiseInstallDefaultSystemAddon(ADDON_ID, "1.0"); + await AddonTestUtils.promiseStartupManager(); + await checkAddon("1.0", BOOTSTRAP_REASONS.ADDON_INSTALL); + + ///// + // Upgrade to a system addon in the profile location, "app-system-addons" + ///// + await promiseUpdateSystemAddon(ADDON_ID, "2.0"); + await checkAddon("2.0", BOOTSTRAP_REASONS.ADDON_UPGRADE); + + ///// + // Builtin system addon is changed, it has precedence because when this + // happens we remove all prior system addon upgrades. + ///// + await AddonTestUtils.promiseShutdownManager(); + await AddonTestUtils.overrideBuiltIns(builtInOverride); + await promiseInstallDefaultSystemAddon(ADDON_ID, "1.5"); + await AddonTestUtils.promiseStartupManager("2"); + await checkAddon( + "1.5", + BOOTSTRAP_REASONS.ADDON_DOWNGRADE, + BOOTSTRAP_REASONS.APP_STARTUP + ); + + // cleanup the system addon in the default location + await AddonTestUtils.manuallyUninstall(systemDefaults, ADDON_ID); + await AddonTestUtils.promiseRestartManager(); + + await AddonTestUtils.promiseShutdownManager(); + BootstrapMonitor.clear(ADDON_ID); +}); + +add_task(async function test_builtin_addon_version_precedence() { + builtInOverride.system = []; + + await AddonTestUtils.promiseStartupManager(); + await Promise.all([ + installBuiltinExtension({ + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { id: ADDON_ID }, + }, + }, + }), + AddonTestUtils.promiseWebExtensionStartup(ADDON_ID), + ]); + await checkAddon("1.0", BOOTSTRAP_REASONS.ADDON_INSTALL); + + ///// + // Upgrade to a system addon in the profile location, "app-system-addons" + ///// + await promiseUpdateSystemAddon(ADDON_ID, "2.0"); + await checkAddon("2.0", BOOTSTRAP_REASONS.ADDON_UPGRADE); + + ///// + // Builtin addon is changed, the system addon should still have precedence. + ///// + await Promise.all([ + installBuiltinExtension( + { + manifest: { + version: "1.5", + browser_specific_settings: { + gecko: { id: ADDON_ID }, + }, + }, + }, + false + ), + AddonTestUtils.promiseAddonEvent("onInstalled"), + ]); + await checkAddon("2.0", BOOTSTRAP_REASONS.ADDON_UPGRADE); + + let addon = await AddonManager.getAddonByID(ADDON_ID); + await addon.uninstall(); + await AddonTestUtils.promiseShutdownManager(); + BootstrapMonitor.clear(ADDON_ID); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_systemaddomstartupprefs.js b/toolkit/mozapps/extensions/test/xpcshell/test_systemaddomstartupprefs.js new file mode 100644 index 0000000000..46a53917f9 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_systemaddomstartupprefs.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies that system addon about:config prefs +// are honored during startup/restarts/upgrades. +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2"); + +let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); +distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", distroDir); + +AddonTestUtils.usePrivilegedSignatures = "system"; + +add_task(initSystemAddonDirs); + +BootstrapMonitor.init(); + +add_task(async function setup() { + let xpi = await getSystemAddonXPI(1, "1.0"); + await AddonTestUtils.manuallyInstall(xpi, distroDir); +}); + +add_task(async function systemAddonPreffedOff() { + const id = "system1@tests.mozilla.org"; + Services.prefs.setBoolPref("extensions.system1.enabled", false); + + await overrideBuiltIns({ system: [id] }); + + await promiseStartupManager(); + + BootstrapMonitor.checkInstalled(id); + BootstrapMonitor.checkNotStarted(id); + + await promiseRestartManager(); + + BootstrapMonitor.checkNotStarted(id); + + await promiseShutdownManager({ clearOverrides: false }); +}); + +add_task(async function systemAddonPreffedOn() { + const id = "system1@tests.mozilla.org"; + Services.prefs.setBoolPref("extensions.system1.enabled", true); + + await promiseStartupManager("2.0"); + + BootstrapMonitor.checkInstalled(id); + BootstrapMonitor.checkStarted(id); + + await promiseRestartManager(); + + BootstrapMonitor.checkStarted(id); + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js b/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js new file mode 100644 index 0000000000..80faa57fc1 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js @@ -0,0 +1,765 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const ID = "addon@tests.mozilla.org"; + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +function waitForBootstrapEvent(expectedEvent, addonId) { + return new Promise(resolve => { + function listener(msg, { method, params, reason }) { + if (params.id === addonId && method === expectedEvent) { + resolve({ params, method, reason }); + AddonTestUtils.off("bootstrap-method", listener); + } else { + info(`Ignoring bootstrap event: ${method} for ${params.id}`); + } + } + AddonTestUtils.on("bootstrap-method", listener); + }); +} + +async function checkEvent(promise, { reason, params }) { + let event = await promise; + info(`Checking bootstrap event ${event.method} for ${event.params.id}`); + + equal( + event.reason, + reason, + `Expected bootstrap reason ${getReasonName(reason)} got ${getReasonName( + event.reason + )}` + ); + + for (let [param, value] of Object.entries(params)) { + equal(event.params[param], value, `Expected value for params.${param}`); + } +} + +BootstrapMonitor.init(); + +const XPIS = {}; + +add_task(async function setup() { + for (let n of [1, 2]) { + XPIS[n] = await createTempWebExtensionFile({ + manifest: { + name: "Test", + version: `${n}.0`, + browser_specific_settings: { gecko: { id: ID } }, + }, + }); + } +}); + +// Install a temporary add-on with no existing add-on present. +// Restart and make sure it has gone away. +add_task(async function test_new_temporary() { + await promiseStartupManager(); + + let extInstallCalled = false; + AddonManager.addInstallListener({ + onExternalInstall: aInstall => { + Assert.equal(aInstall.id, ID); + Assert.equal(aInstall.version, "1.0"); + extInstallCalled = true; + }, + }); + + let installingCalled = false; + let installedCalled = false; + AddonManager.addAddonListener({ + onInstalling: aInstall => { + Assert.equal(aInstall.id, ID); + Assert.equal(aInstall.version, "1.0"); + installingCalled = true; + }, + onInstalled: aInstall => { + Assert.equal(aInstall.id, ID); + Assert.equal(aInstall.version, "1.0"); + installedCalled = true; + }, + onInstallStarted: aInstall => { + do_throw("onInstallStarted called unexpectedly"); + }, + }); + + await AddonManager.installTemporaryAddon(XPIS[1]); + + Assert.ok(extInstallCalled); + Assert.ok(installingCalled); + Assert.ok(installedCalled); + + const install = BootstrapMonitor.checkInstalled(ID, "1.0"); + equal(install.reason, BOOTSTRAP_REASONS.ADDON_INSTALL); + + BootstrapMonitor.checkStarted(ID, "1.0"); + + let info = BootstrapMonitor.started.get(ID); + Assert.equal(info.reason, BOOTSTRAP_REASONS.ADDON_INSTALL); + + let addon = await promiseAddonByID(ID); + + checkAddon(ID, addon, { + version: "1.0", + name: "Test", + isCompatible: true, + appDisabled: false, + isActive: true, + type: "extension", + signedState: AddonManager.SIGNEDSTATE_PRIVILEGED, + temporarilyInstalled: true, + }); + + let onShutdown = waitForBootstrapEvent("shutdown", ID); + let onUninstall = waitForBootstrapEvent("uninstall", ID); + + await promiseRestartManager(); + + let shutdown = await onShutdown; + equal(shutdown.reason, BOOTSTRAP_REASONS.ADDON_UNINSTALL); + + let uninstall = await onUninstall; + equal(uninstall.reason, BOOTSTRAP_REASONS.ADDON_UNINSTALL); + + BootstrapMonitor.checkNotInstalled(ID); + BootstrapMonitor.checkNotStarted(ID); + + addon = await promiseAddonByID(ID); + Assert.equal(addon, null); + + await promiseRestartManager(); +}); + +// Install a temporary add-on over the top of an existing add-on. +// Restart and make sure the existing add-on comes back. +add_task(async function test_replace_temporary() { + await promiseInstallFile(XPIS[2]); + let addon = await promiseAddonByID(ID); + + BootstrapMonitor.checkInstalled(ID, "2.0"); + BootstrapMonitor.checkStarted(ID, "2.0"); + + checkAddon(ID, addon, { + version: "2.0", + name: "Test", + isCompatible: true, + appDisabled: false, + isActive: true, + type: "extension", + signedState: AddonManager.SIGNEDSTATE_PRIVILEGED, + temporarilyInstalled: false, + }); + + let tempdir = gTmpD.clone(); + + for (let newversion of ["1.0", "3.0"]) { + for (let packed of [false, true]) { + // ugh, file locking issues with xpis on windows + if (packed && AppConstants.platform == "win") { + continue; + } + + let files = ExtensionTestCommon.generateFiles({ + manifest: { + name: "Test", + version: newversion, + browser_specific_settings: { gecko: { id: ID } }, + }, + }); + + let target = await AddonTestUtils.promiseWriteFilesToExtension( + tempdir.path, + ID, + files, + !packed + ); + + let onShutdown = waitForBootstrapEvent("shutdown", ID); + let onUpdate = waitForBootstrapEvent("update", ID); + let onStartup = waitForBootstrapEvent("startup", ID); + + await AddonManager.installTemporaryAddon(target); + + let reason = + Services.vc.compare(newversion, "2.0") < 0 + ? BOOTSTRAP_REASONS.ADDON_DOWNGRADE + : BOOTSTRAP_REASONS.ADDON_UPGRADE; + + await checkEvent(onShutdown, { + reason, + params: { + version: "2.0", + }, + }); + + await checkEvent(onUpdate, { + reason, + params: { + version: newversion, + oldVersion: "2.0", + }, + }); + + await checkEvent(onStartup, { + reason, + params: { + version: newversion, + oldVersion: "2.0", + }, + }); + + addon = await promiseAddonByID(ID); + + let signedState = packed + ? AddonManager.SIGNEDSTATE_PRIVILEGED + : AddonManager.SIGNEDSTATE_UNKNOWN; + + // temporary add-on is installed and started + checkAddon(ID, addon, { + version: newversion, + name: "Test", + isCompatible: true, + appDisabled: false, + isActive: true, + type: "extension", + signedState, + temporarilyInstalled: true, + }); + + // Now restart, the temporary addon will go away which should + // be the opposite action (ie, if the temporary addon was an + // upgrade, then removing it is a downgrade and vice versa) + reason = + reason == BOOTSTRAP_REASONS.ADDON_UPGRADE + ? BOOTSTRAP_REASONS.ADDON_DOWNGRADE + : BOOTSTRAP_REASONS.ADDON_UPGRADE; + + onShutdown = waitForBootstrapEvent("shutdown", ID); + onUpdate = waitForBootstrapEvent("update", ID); + onStartup = waitForBootstrapEvent("startup", ID); + + await promiseRestartManager(); + + await checkEvent(onShutdown, { + reason, + params: { + version: newversion, + }, + }); + + await checkEvent(onUpdate, { + reason, + params: { + version: "2.0", + oldVersion: newversion, + }, + }); + + await checkEvent(onStartup, { + // We don't actually propagate the upgrade/downgrade reason across + // the browser restart when a temporary addon is removed. See + // bug 1359558 for detailed reasoning. + reason: BOOTSTRAP_REASONS.APP_STARTUP, + params: { + version: "2.0", + }, + }); + + BootstrapMonitor.checkInstalled(ID, "2.0"); + BootstrapMonitor.checkStarted(ID, "2.0"); + + addon = await promiseAddonByID(ID); + + // existing add-on is back + checkAddon(ID, addon, { + version: "2.0", + name: "Test", + isCompatible: true, + appDisabled: false, + isActive: true, + type: "extension", + signedState: AddonManager.SIGNEDSTATE_PRIVILEGED, + temporarilyInstalled: false, + }); + + Services.obs.notifyObservers(target, "flush-cache-entry"); + target.remove(true); + } + } + + // remove original add-on + await addon.uninstall(); + + BootstrapMonitor.checkNotInstalled(ID); + BootstrapMonitor.checkNotStarted(ID); + + await promiseRestartManager(); +}); + +// Test that loading from the same path multiple times work +add_task(async function test_samefile() { + // File locking issues on Windows, ugh + if (AppConstants.platform == "win") { + return; + } + + // test that a webextension works + let webext = createTempWebExtensionFile({ + manifest: { + version: "1.0", + name: "Test WebExtension 1 (temporary)", + browser_specific_settings: { + gecko: { + id: ID, + }, + }, + }, + }); + + let addon = await AddonManager.installTemporaryAddon(webext); + + // temporary add-on is installed and started + checkAddon(ID, addon, { + version: "1.0", + name: "Test WebExtension 1 (temporary)", + isCompatible: true, + appDisabled: false, + isActive: true, + type: "extension", + signedState: AddonManager.SIGNEDSTATE_PRIVILEGED, + temporarilyInstalled: true, + }); + + Services.obs.notifyObservers(webext, "flush-cache-entry"); + webext.remove(false); + webext = createTempWebExtensionFile({ + manifest: { + version: "2.0", + name: "Test WebExtension 1 (temporary)", + browser_specific_settings: { + gecko: { + id: ID, + }, + }, + }, + }); + + addon = await AddonManager.installTemporaryAddon(webext); + + // temporary add-on is installed and started + checkAddon(ID, addon, { + version: "2.0", + name: "Test WebExtension 1 (temporary)", + isCompatible: true, + appDisabled: false, + isActive: true, + type: "extension", + isWebExtension: true, + signedState: AddonManager.SIGNEDSTATE_PRIVILEGED, + temporarilyInstalled: true, + }); + + await addon.uninstall(); +}); + +// Install a temporary add-on over the top of an existing add-on. +// Uninstall it and make sure the existing add-on comes back. +add_task(async function test_replace_permanent() { + await promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { gecko: { id: ID } }, + version: "1.0", + name: "Test Bootstrap 1", + }, + }); + + BootstrapMonitor.checkInstalled(ID, "1.0"); + BootstrapMonitor.checkStarted(ID, "1.0"); + + let unpacked_addon = gTmpD.clone(); + unpacked_addon.append(ID); + + let files = ExtensionTestCommon.generateFiles({ + manifest: { + browser_specific_settings: { gecko: { id: ID } }, + version: "2.0", + name: "Test Bootstrap 1 (temporary)", + }, + }); + await AddonTestUtils.promiseWriteFilesToDir(unpacked_addon.path, files); + + let extInstallCalled = false; + AddonManager.addInstallListener({ + onExternalInstall: aInstall => { + Assert.equal(aInstall.id, ID); + Assert.equal(aInstall.version, "2.0"); + extInstallCalled = true; + }, + }); + + let installingCalled = false; + let installedCalled = false; + AddonManager.addAddonListener({ + onInstalling: aInstall => { + Assert.equal(aInstall.id, ID); + if (!installingCalled) { + Assert.equal(aInstall.version, "2.0"); + } + installingCalled = true; + }, + onInstalled: aInstall => { + Assert.equal(aInstall.id, ID); + if (!installedCalled) { + Assert.equal(aInstall.version, "2.0"); + } + installedCalled = true; + }, + onInstallStarted: aInstall => { + do_throw("onInstallStarted called unexpectedly"); + }, + }); + + let addon = await AddonManager.installTemporaryAddon(unpacked_addon); + + Assert.ok(extInstallCalled); + Assert.ok(installingCalled); + Assert.ok(installedCalled); + + BootstrapMonitor.checkInstalled(ID); + BootstrapMonitor.checkStarted(ID); + + // temporary add-on is installed and started + checkAddon(ID, addon, { + version: "2.0", + name: "Test Bootstrap 1 (temporary)", + isCompatible: true, + appDisabled: false, + isActive: true, + type: "extension", + signedState: AddonManager.SIGNEDSTATE_UNKNOWN, + temporarilyInstalled: true, + }); + + await addon.uninstall(); + + BootstrapMonitor.checkInstalled(ID); + BootstrapMonitor.checkStarted(ID); + + addon = await promiseAddonByID(ID); + + // existing add-on is back + checkAddon(ID, addon, { + version: "1.0", + name: "Test Bootstrap 1", + isCompatible: true, + appDisabled: false, + isActive: true, + type: "extension", + signedState: AddonManager.SIGNEDSTATE_PRIVILEGED, + temporarilyInstalled: false, + }); + + unpacked_addon.remove(true); + await addon.uninstall(); + + BootstrapMonitor.checkNotInstalled(ID); + BootstrapMonitor.checkNotStarted(ID); + + await promiseRestartManager(); +}); + +// Install a temporary add-on as a version upgrade over the top of an +// existing temporary add-on. +add_task(async function test_replace_temporary() { + const unpackedAddon = gTmpD.clone(); + unpackedAddon.append(ID); + + let files = ExtensionTestCommon.generateFiles({ + manifest: { + browser_specific_settings: { gecko: { id: ID } }, + version: "1.0", + }, + }); + await AddonTestUtils.promiseWriteFilesToDir(unpackedAddon.path, files); + + await AddonManager.installTemporaryAddon(unpackedAddon); + + // Increment the version number, re-install it, and make sure it + // gets marked as an upgrade. + files = ExtensionTestCommon.generateFiles({ + manifest: { + browser_specific_settings: { gecko: { id: ID } }, + version: "2.0", + }, + }); + await AddonTestUtils.promiseWriteFilesToDir(unpackedAddon.path, files); + + const onShutdown = waitForBootstrapEvent("shutdown", ID); + const onUpdate = waitForBootstrapEvent("update", ID); + const onStartup = waitForBootstrapEvent("startup", ID); + await AddonManager.installTemporaryAddon(unpackedAddon); + + await checkEvent(onShutdown, { + reason: BOOTSTRAP_REASONS.ADDON_UPGRADE, + params: { + version: "1.0", + }, + }); + + await checkEvent(onUpdate, { + reason: BOOTSTRAP_REASONS.ADDON_UPGRADE, + params: { + version: "2.0", + oldVersion: "1.0", + }, + }); + + await checkEvent(onStartup, { + reason: BOOTSTRAP_REASONS.ADDON_UPGRADE, + params: { + version: "2.0", + oldVersion: "1.0", + }, + }); + + const addon = await promiseAddonByID(ID); + await addon.uninstall(); + + unpackedAddon.remove(true); + await promiseRestartManager(); +}); + +// Install a temporary add-on as a version downgrade over the top of an +// existing temporary add-on. +add_task(async function test_replace_temporary_downgrade() { + const unpackedAddon = gTmpD.clone(); + unpackedAddon.append(ID); + + let files = ExtensionTestCommon.generateFiles({ + manifest: { + browser_specific_settings: { gecko: { id: ID } }, + version: "1.0", + }, + }); + await AddonTestUtils.promiseWriteFilesToDir(unpackedAddon.path, files); + + await AddonManager.installTemporaryAddon(unpackedAddon); + + // Decrement the version number, re-install, and make sure + // it gets marked as a downgrade. + files = ExtensionTestCommon.generateFiles({ + manifest: { + browser_specific_settings: { gecko: { id: ID } }, + version: "0.8", + }, + }); + await AddonTestUtils.promiseWriteFilesToDir(unpackedAddon.path, files); + + const onShutdown = waitForBootstrapEvent("shutdown", ID); + const onUpdate = waitForBootstrapEvent("update", ID); + const onStartup = waitForBootstrapEvent("startup", ID); + await AddonManager.installTemporaryAddon(unpackedAddon); + + await checkEvent(onShutdown, { + reason: BOOTSTRAP_REASONS.ADDON_DOWNGRADE, + params: { + version: "1.0", + }, + }); + + await checkEvent(onUpdate, { + reason: BOOTSTRAP_REASONS.ADDON_DOWNGRADE, + params: { + oldVersion: "1.0", + version: "0.8", + }, + }); + + await checkEvent(onStartup, { + reason: BOOTSTRAP_REASONS.ADDON_DOWNGRADE, + params: { + version: "0.8", + }, + }); + + const addon = await promiseAddonByID(ID); + await addon.uninstall(); + + unpackedAddon.remove(true); + await promiseRestartManager(); +}); + +// Installing a temporary add-on over an existing add-on with the same +// version number should be installed as an upgrade. +add_task(async function test_replace_same_version() { + const unpackedAddon = gTmpD.clone(); + unpackedAddon.append(ID); + + let files = ExtensionTestCommon.generateFiles({ + manifest: { + browser_specific_settings: { gecko: { id: ID } }, + version: "1.0", + }, + }); + await AddonTestUtils.promiseWriteFilesToDir(unpackedAddon.path, files); + + const onInitialInstall = waitForBootstrapEvent("install", ID); + const onInitialStartup = waitForBootstrapEvent("startup", ID); + await AddonManager.installTemporaryAddon(unpackedAddon); + + await checkEvent(onInitialInstall, { + reason: BOOTSTRAP_REASONS.ADDON_INSTALL, + params: { + version: "1.0", + }, + }); + + await checkEvent(onInitialStartup, { + reason: BOOTSTRAP_REASONS.ADDON_INSTALL, + params: { + version: "1.0", + }, + }); + + let info = BootstrapMonitor.started.get(ID); + Assert.equal(info.reason, BOOTSTRAP_REASONS.ADDON_INSTALL); + + // Install it again. + const onShutdown = waitForBootstrapEvent("shutdown", ID); + const onUpdate = waitForBootstrapEvent("update", ID); + const onStartup = waitForBootstrapEvent("startup", ID); + await AddonManager.installTemporaryAddon(unpackedAddon); + + await checkEvent(onShutdown, { + reason: BOOTSTRAP_REASONS.ADDON_UPGRADE, + params: { + version: "1.0", + }, + }); + + await checkEvent(onUpdate, { + reason: BOOTSTRAP_REASONS.ADDON_UPGRADE, + params: { + oldVersion: "1.0", + version: "1.0", + }, + }); + + await checkEvent(onStartup, { + reason: BOOTSTRAP_REASONS.ADDON_UPGRADE, + params: { + version: "1.0", + }, + }); + + const addon = await promiseAddonByID(ID); + await addon.uninstall(); + + unpackedAddon.remove(true); + await promiseRestartManager(); +}); + +// Install a temporary add-on over the top of an existing disabled add-on. +// After restart, the existing add-on should continue to be installed and disabled. +add_task(async function test_replace_permanent_disabled() { + await promiseInstallFile(XPIS[1]); + let addon = await promiseAddonByID(ID); + + BootstrapMonitor.checkInstalled(ID, "1.0"); + BootstrapMonitor.checkStarted(ID, "1.0"); + + await addon.disable(); + + BootstrapMonitor.checkInstalled(ID, "1.0"); + BootstrapMonitor.checkNotStarted(ID); + + let unpacked_addon = gTmpD.clone(); + unpacked_addon.append(ID); + + let files = ExtensionTestCommon.generateFiles({ + manifest: { + browser_specific_settings: { gecko: { id: ID } }, + name: "Test", + version: "2.0", + }, + }); + await AddonTestUtils.promiseWriteFilesToDir(unpacked_addon.path, files); + + let extInstallCalled = false; + AddonManager.addInstallListener({ + onExternalInstall: aInstall => { + Assert.equal(aInstall.id, ID); + Assert.equal(aInstall.version, "2.0"); + extInstallCalled = true; + }, + }); + + let tempAddon = await AddonManager.installTemporaryAddon(unpacked_addon); + + Assert.ok(extInstallCalled); + + BootstrapMonitor.checkInstalled(ID, "2.0"); + BootstrapMonitor.checkStarted(ID); + + // temporary add-on is installed and started + checkAddon(ID, tempAddon, { + version: "2.0", + name: "Test", + isCompatible: true, + appDisabled: false, + isActive: true, + type: "extension", + signedState: AddonManager.SIGNEDSTATE_UNKNOWN, + temporarilyInstalled: true, + }); + + await tempAddon.uninstall(); + unpacked_addon.remove(true); + + await addon.enable(); + await new Promise(executeSoon); + addon = await promiseAddonByID(ID); + + BootstrapMonitor.checkInstalled(ID, "1.0"); + BootstrapMonitor.checkStarted(ID); + + // existing add-on is back + checkAddon(ID, addon, { + version: "1.0", + name: "Test", + isCompatible: true, + appDisabled: false, + isActive: true, + type: "extension", + signedState: AddonManager.SIGNEDSTATE_PRIVILEGED, + temporarilyInstalled: false, + }); + + await addon.uninstall(); + + BootstrapMonitor.checkNotInstalled(ID); + BootstrapMonitor.checkNotStarted(ID); + + await promiseRestartManager(); +}); + +// Tests that XPIs with a .zip extension work when loaded temporarily. +add_task(async function test_zip_extension() { + let xpi = createTempWebExtensionFile({ + background() { + /* globals browser */ + browser.test.sendMessage("started", "Hello."); + }, + }); + xpi.moveTo(null, xpi.leafName.replace(/\.xpi$/, ".zip")); + + let extension = ExtensionTestUtils.loadExtensionXPI(xpi); + await extension.startup(); + + let msg = await extension.awaitMessage("started"); + equal(msg, "Hello.", "Got expected background script message"); + + await extension.unload(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_trash_directory.js b/toolkit/mozapps/extensions/test/xpcshell/test_trash_directory.js new file mode 100644 index 0000000000..2c0540e399 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_trash_directory.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const { BasePromiseWorker } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseWorker.sys.mjs" +); + +// Test that an open file inside the trash directory does not cause +// unrelated installs to break (see bug 1180901 for more background). +add_task(async function test() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + await promiseStartupManager(); + + let profileDir = PathUtils.profileDir; + let trashDir = PathUtils.join(profileDir, "extensions", "trash"); + let testFile = PathUtils.join(trashDir, "test.txt"); + + await IOUtils.makeDirectory(trashDir); + + let trashDirExists = await IOUtils.exists(trashDir); + ok(trashDirExists, "trash directory should have been created"); + + await IOUtils.writeUTF8(testFile, ""); + + // Use a worker to keep the testFile open. + const worker = new BasePromiseWorker( + "resource://test/data/test_trash_directory.worker.js" + ); + await worker.post("open", [testFile]); + + let fileExists = await IOUtils.exists(testFile); + ok(fileExists, "test.txt should have been created in " + trashDir); + + await promiseInstallWebExtension({}); + + // The testFile should still exist at this point because we have not + // yet closed the file handle and as a result, Windows cannot remove it. + fileExists = await IOUtils.exists(testFile); + ok(fileExists, "test.txt should still exist"); + + // Cleanup + await promiseShutdownManager(); + await worker.post("close", []); + await IOUtils.remove(testFile); + await IOUtils.remove(trashDir, { recursive: true }); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_types.js b/toolkit/mozapps/extensions/test/xpcshell/test_types.js new file mode 100644 index 0000000000..b9a0d3987e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_types.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies that custom types can be defined and undefined + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + +add_task(async function setup() { + await promiseStartupManager(); +}); + +add_task(async function test_new_addonType() { + Assert.equal(false, AddonManager.hasAddonType("test")); + + // The dumbest provider possible + const provider = {}; + + AddonManagerPrivate.registerProvider(provider, ["test"]); + + Assert.equal(true, AddonManager.hasAddonType("test")); + Assert.equal(false, AddonManager.hasAddonType("t$e%st")); + Assert.equal(false, AddonManager.hasAddonType(null)); + Assert.equal(false, AddonManager.hasAddonType(undefined)); + + AddonManagerPrivate.unregisterProvider(provider); + + Assert.equal(false, AddonManager.hasAddonType("test")); +}); + +add_task(async function test_bad_addonType() { + const provider = {}; + Assert.throws( + () => AddonManagerPrivate.registerProvider(provider, /* addonTypes =*/ {}), + /aTypes must be an array or null/ + ); + + Assert.throws( + () => AddonManagerPrivate.registerProvider(provider, new Set()), + /aTypes must be an array or null/ + ); +}); + +add_task(async function test_addonTypes_should_be_immutable() { + const provider = {}; + const addonTypes = []; + + addonTypes.push("test"); + AddonManagerPrivate.registerProvider(provider, addonTypes); + addonTypes.pop(); + addonTypes.push("test_added"); + // Modifications to addonTypes should not affect AddonManager. + Assert.equal(true, AddonManager.hasAddonType("test")); + Assert.equal(false, AddonManager.hasAddonType("test_added")); + AddonManagerPrivate.unregisterProvider(provider); + + AddonManagerPrivate.registerProvider(provider, addonTypes); + // After re-registering the provider, the type change should have been processed. + Assert.equal(false, AddonManager.hasAddonType("test")); + Assert.equal(true, AddonManager.hasAddonType("test_added")); + AddonManagerPrivate.unregisterProvider(provider); +}); + +add_task(async function test_missing_addonType() { + const dummyAddon = { + id: "some dummy addon from provider without .addonTypes property", + }; + const provider = { + // addonTypes Set is missing. This only happens in unit tests, but let's + // verify that the implementation behaves reasonably. + // A provider without an explicitly registered type may still return an + // entry when getAddonsByTypes is called. + async getAddonsByTypes(types) { + Assert.equal(null, types); + return [dummyAddon]; + }, + }; + + AddonManagerPrivate.registerProvider(provider); // addonTypes not set. + Assert.equal(false, AddonManager.hasAddonType("test")); + Assert.equal(false, AddonManager.hasAddonType(null)); + Assert.equal(false, AddonManager.hasAddonType(undefined)); + + const addons = await AddonManager.getAddonsByTypes(null); + Assert.equal(1, addons.length); + Assert.equal(dummyAddon, addons[0]); + + AddonManagerPrivate.unregisterProvider(provider); +}); + +add_task(async function test_getAddonTypesByProvider() { + let defaultTypes = AddonManagerPrivate.getAddonTypesByProvider("XPIProvider"); + Assert.ok(defaultTypes.includes("extension"), `extension in ${defaultTypes}`); + Assert.throws( + () => AddonManagerPrivate.getAddonTypesByProvider(), + /No addonTypes found for provider: undefined/ + ); + Assert.throws( + () => AddonManagerPrivate.getAddonTypesByProvider("MaybeExistent"), + /No addonTypes found for provider: MaybeExistent/ + ); + + const provider = { name: "MaybeExistent" }; + AddonManagerPrivate.registerProvider(provider, ["test"]); + Assert.deepEqual( + AddonManagerPrivate.getAddonTypesByProvider("MaybeExistent"), + ["test"], + "Newly registered type returned by getAddonTypesByProvider" + ); + + AddonManagerPrivate.unregisterProvider(provider); + + Assert.throws( + () => AddonManagerPrivate.getAddonTypesByProvider("MaybeExistent"), + /No addonTypes found for provider: MaybeExistent/ + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_undouninstall.js b/toolkit/mozapps/extensions/test/xpcshell/test_undouninstall.js new file mode 100644 index 0000000000..af07fb1e34 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_undouninstall.js @@ -0,0 +1,584 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies that forcing undo for uninstall works + +const APP_STARTUP = 1; +const APP_SHUTDOWN = 2; +const ADDON_DISABLE = 4; +const ADDON_INSTALL = 5; +const ADDON_UNINSTALL = 6; +const ADDON_UPGRADE = 7; + +const ID = "undouninstall1@tests.mozilla.org"; +const INCOMPAT_ID = "incompatible@tests.mozilla.org"; + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +const ADDONS = { + test_undoincompatible: { + manifest: { + name: "Incompatible Addon", + browser_specific_settings: { + gecko: { + id: "incompatible@tests.mozilla.org", + strict_min_version: "2", + strict_max_version: "2", + }, + }, + }, + }, + test_undouninstall1: { + manifest: { + name: "Test Bootstrap 1", + browser_specific_settings: { + gecko: { + id: "undouninstall1@tests.mozilla.org", + }, + }, + }, + }, +}; + +const XPIS = {}; + +BootstrapMonitor.init(); + +function getStartupReason(id) { + let info = BootstrapMonitor.started.get(id); + return info ? info.reason : undefined; +} + +function getShutdownReason(id) { + let info = BootstrapMonitor.stopped.get(id); + return info ? info.reason : undefined; +} + +function getInstallReason(id) { + let info = BootstrapMonitor.installed.get(id); + return info ? info.reason : undefined; +} + +function getUninstallReason(id) { + let info = BootstrapMonitor.uninstalled.get(id); + return info ? info.reason : undefined; +} + +function getShutdownNewVersion(id) { + let info = BootstrapMonitor.stopped.get(id); + return info ? info.params.newVersion : undefined; +} + +// Sets up the profile by installing an add-on. +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + + await promiseStartupManager(); + registerCleanupFunction(promiseShutdownManager); + + for (let [name, files] of Object.entries(ADDONS)) { + XPIS[name] = await AddonTestUtils.createTempWebExtensionFile(files); + } +}); + +// Tests that an enabled restartless add-on can be uninstalled and goes away +// when the uninstall is committed +add_task(async function uninstallRestartless() { + await promiseInstallFile(XPIS.test_undouninstall1); + + let a1 = await promiseAddonByID(ID); + + Assert.notEqual(a1, null); + BootstrapMonitor.checkInstalled(ID, "1.0"); + BootstrapMonitor.checkStarted(ID, "1.0"); + Assert.equal(getInstallReason(ID), ADDON_INSTALL); + Assert.equal(getStartupReason(ID), ADDON_INSTALL); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(a1.isActive); + Assert.ok(!a1.userDisabled); + + await a1.uninstall(true); + + a1 = await promiseAddonByID(ID); + + Assert.notEqual(a1, null); + BootstrapMonitor.checkInstalled(ID); + BootstrapMonitor.checkNotStarted(ID); + Assert.equal(getShutdownReason(ID), ADDON_UNINSTALL); + Assert.ok(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations)); + Assert.ok(!a1.isActive); + Assert.ok(!a1.userDisabled); + + await a1.uninstall(); + + a1 = await promiseAddonByID(ID); + + Assert.equal(a1, null); + BootstrapMonitor.checkNotStarted(ID); +}); + +// Tests that an enabled restartless add-on can be uninstalled and then cancelled +add_task(async function cancelUninstallOfRestartless() { + await promiseInstallFile(XPIS.test_undouninstall1); + let a1 = await promiseAddonByID(ID); + + Assert.notEqual(a1, null); + BootstrapMonitor.checkInstalled(ID, "1.0"); + BootstrapMonitor.checkStarted(ID, "1.0"); + Assert.equal(getInstallReason(ID), ADDON_INSTALL); + Assert.equal(getStartupReason(ID), ADDON_INSTALL); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(a1.isActive); + Assert.ok(!a1.userDisabled); + + await expectEvents( + { + addonEvents: { + "undouninstall1@tests.mozilla.org": [{ event: "onUninstalling" }], + }, + }, + () => a1.uninstall(true) + ); + + a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + + Assert.notEqual(a1, null); + BootstrapMonitor.checkInstalled(ID); + BootstrapMonitor.checkNotStarted(ID); + Assert.equal(getShutdownReason(ID), ADDON_UNINSTALL); + Assert.ok(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations)); + Assert.ok(!a1.isActive); + Assert.ok(!a1.userDisabled); + + let promises = [ + promiseAddonEvent("onOperationCancelled"), + promiseWebExtensionStartup(ID), + ]; + a1.cancelUninstall(); + await Promise.all(promises); + + BootstrapMonitor.checkInstalled(ID, "1.0"); + BootstrapMonitor.checkStarted(ID, "1.0"); + Assert.equal(getStartupReason(ID), ADDON_INSTALL); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(a1.isActive); + Assert.ok(!a1.userDisabled); + + await promiseShutdownManager(); + + Assert.equal(getShutdownReason(ID), APP_SHUTDOWN); + Assert.equal(getShutdownNewVersion(ID), undefined); + + await promiseStartupManager(); + + a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + + Assert.notEqual(a1, null); + BootstrapMonitor.checkStarted(ID, "1.0"); + Assert.equal(getStartupReason(ID), APP_STARTUP); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(a1.isActive); + Assert.ok(!a1.userDisabled); + + await a1.uninstall(); +}); + +// Tests that reinstalling an enabled restartless add-on waiting to be +// uninstalled aborts the uninstall and leaves the add-on enabled +add_task(async function reinstallAddonAwaitingUninstall() { + await promiseInstallFile(XPIS.test_undouninstall1); + + let a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + + Assert.notEqual(a1, null); + BootstrapMonitor.checkInstalled(ID, "1.0"); + BootstrapMonitor.checkStarted(ID, "1.0"); + Assert.equal(getInstallReason(ID), ADDON_INSTALL); + Assert.equal(getStartupReason(ID), ADDON_INSTALL); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(a1.isActive); + Assert.ok(!a1.userDisabled); + + await expectEvents( + { + addonEvents: { + "undouninstall1@tests.mozilla.org": [{ event: "onUninstalling" }], + }, + }, + () => a1.uninstall(true) + ); + + a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + + Assert.notEqual(a1, null); + BootstrapMonitor.checkInstalled(ID); + BootstrapMonitor.checkNotStarted(ID); + Assert.equal(getShutdownReason(ID), ADDON_UNINSTALL); + Assert.ok(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations)); + Assert.ok(!a1.isActive); + Assert.ok(!a1.userDisabled); + + await expectEvents( + { + addonEvents: { + "undouninstall1@tests.mozilla.org": [ + { event: "onInstalling" }, + { event: "onInstalled" }, + ], + }, + installEvents: [ + { event: "onNewInstall" }, + { event: "onInstallStarted" }, + { event: "onInstallEnded" }, + ], + }, + () => promiseInstallFile(XPIS.test_undouninstall1) + ); + + a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + + BootstrapMonitor.checkInstalled(ID, "1.0"); + BootstrapMonitor.checkStarted(ID, "1.0"); + Assert.equal(getInstallReason(ID), ADDON_UPGRADE); + Assert.equal(getStartupReason(ID), ADDON_UPGRADE); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(a1.isActive); + Assert.ok(!a1.userDisabled); + + await promiseShutdownManager(); + + Assert.equal(getShutdownReason(ID), APP_SHUTDOWN); + + await promiseStartupManager(); + + a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + + Assert.notEqual(a1, null); + BootstrapMonitor.checkStarted(ID, "1.0"); + Assert.equal(getStartupReason(ID), APP_STARTUP); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(a1.isActive); + Assert.ok(!a1.userDisabled); + + await a1.uninstall(); +}); + +// Tests that a disabled restartless add-on can be uninstalled and goes away +// when the uninstall is committed +add_task(async function uninstallDisabledRestartless() { + await promiseInstallFile(XPIS.test_undouninstall1); + + let a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + + Assert.notEqual(a1, null); + BootstrapMonitor.checkInstalled(ID, "1.0"); + BootstrapMonitor.checkStarted(ID, "1.0"); + Assert.equal(getInstallReason(ID), ADDON_INSTALL); + Assert.equal(getStartupReason(ID), ADDON_INSTALL); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(a1.isActive); + Assert.ok(!a1.userDisabled); + + await a1.disable(); + BootstrapMonitor.checkNotStarted(ID); + Assert.equal(getShutdownReason(ID), ADDON_DISABLE); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(!a1.isActive); + Assert.ok(a1.userDisabled); + + await expectEvents( + { + addonEvents: { + "undouninstall1@tests.mozilla.org": [{ event: "onUninstalling" }], + }, + }, + () => a1.uninstall(true) + ); + + a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + + Assert.notEqual(a1, null); + BootstrapMonitor.checkNotStarted(ID); + Assert.ok(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations)); + Assert.ok(!a1.isActive); + Assert.ok(a1.userDisabled); + + // commit the uninstall + await expectEvents( + { + addonEvents: { + "undouninstall1@tests.mozilla.org": [{ event: "onUninstalled" }], + }, + }, + () => a1.uninstall() + ); + + a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + + Assert.equal(a1, null); + BootstrapMonitor.checkNotStarted(ID); + BootstrapMonitor.checkNotInstalled(ID); + Assert.equal(getUninstallReason(ID), ADDON_UNINSTALL); +}); + +// Tests that a disabled restartless add-on can be uninstalled and then cancelled +add_task(async function cancelUninstallDisabledRestartless() { + await expectEvents( + { + addonEvents: { + "undouninstall1@tests.mozilla.org": [ + { event: "onInstalling" }, + { event: "onInstalled" }, + ], + }, + installEvents: [ + { event: "onNewInstall" }, + { event: "onInstallStarted" }, + { event: "onInstallEnded" }, + ], + }, + () => promiseInstallFile(XPIS.test_undouninstall1) + ); + + let a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + + Assert.notEqual(a1, null); + BootstrapMonitor.checkInstalled(ID, "1.0"); + BootstrapMonitor.checkStarted(ID, "1.0"); + Assert.equal(getInstallReason(ID), ADDON_INSTALL); + Assert.equal(getStartupReason(ID), ADDON_INSTALL); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(a1.isActive); + Assert.ok(!a1.userDisabled); + + await expectEvents( + { + addonEvents: { + "undouninstall1@tests.mozilla.org": [ + { event: "onDisabling" }, + { event: "onDisabled" }, + ], + }, + }, + () => a1.disable() + ); + + BootstrapMonitor.checkNotStarted(ID); + Assert.equal(getShutdownReason(ID), ADDON_DISABLE); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(!a1.isActive); + Assert.ok(a1.userDisabled); + + await expectEvents( + { + addonEvents: { + "undouninstall1@tests.mozilla.org": [{ event: "onUninstalling" }], + }, + }, + () => a1.uninstall(true) + ); + + a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + + Assert.notEqual(a1, null); + BootstrapMonitor.checkNotStarted(ID); + BootstrapMonitor.checkInstalled(ID); + Assert.ok(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations)); + Assert.ok(!a1.isActive); + Assert.ok(a1.userDisabled); + + await expectEvents( + { + addonEvents: { + "undouninstall1@tests.mozilla.org": [{ event: "onOperationCancelled" }], + }, + }, + async () => { + a1.cancelUninstall(); + } + ); + + BootstrapMonitor.checkNotStarted(ID); + BootstrapMonitor.checkInstalled(ID); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(!a1.isActive); + Assert.ok(a1.userDisabled); + + await promiseRestartManager(); + + a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + + Assert.notEqual(a1, null); + BootstrapMonitor.checkNotStarted(ID); + BootstrapMonitor.checkInstalled(ID); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(!a1.isActive); + Assert.ok(a1.userDisabled); + + await a1.uninstall(); +}); + +// Tests that reinstalling a disabled restartless add-on waiting to be +// uninstalled aborts the uninstall and leaves the add-on disabled +add_task(async function reinstallDisabledAddonAwaitingUninstall() { + await promiseInstallFile(XPIS.test_undouninstall1); + + let a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + + Assert.notEqual(a1, null); + BootstrapMonitor.checkInstalled(ID, "1.0"); + BootstrapMonitor.checkStarted(ID, "1.0"); + Assert.equal(getInstallReason(ID), ADDON_INSTALL); + Assert.equal(getStartupReason(ID), ADDON_INSTALL); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(a1.isActive); + Assert.ok(!a1.userDisabled); + + await a1.disable(); + BootstrapMonitor.checkNotStarted(ID); + Assert.equal(getShutdownReason(ID), ADDON_DISABLE); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(!a1.isActive); + Assert.ok(a1.userDisabled); + + await expectEvents( + { + addonEvents: { + "undouninstall1@tests.mozilla.org": [{ event: "onUninstalling" }], + }, + }, + () => a1.uninstall(true) + ); + + a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + + Assert.notEqual(a1, null); + BootstrapMonitor.checkNotStarted(ID); + Assert.ok(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations)); + Assert.ok(!a1.isActive); + Assert.ok(a1.userDisabled); + + await expectEvents( + { + addonEvents: { + "undouninstall1@tests.mozilla.org": [ + { event: "onInstalling" }, + { event: "onInstalled" }, + ], + }, + installEvents: [ + { event: "onNewInstall" }, + { event: "onInstallStarted" }, + { event: "onInstallEnded" }, + ], + }, + () => promiseInstallFile(XPIS.test_undouninstall1) + ); + + a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + + BootstrapMonitor.checkInstalled(ID, "1.0"); + BootstrapMonitor.checkNotStarted(ID, "1.0"); + Assert.equal(getInstallReason(ID), ADDON_UPGRADE); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(!a1.isActive); + Assert.ok(a1.userDisabled); + + await promiseRestartManager(); + + a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + + Assert.notEqual(a1, null); + BootstrapMonitor.checkNotStarted(ID, "1.0"); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(!a1.isActive); + Assert.ok(a1.userDisabled); + + await a1.uninstall(); +}); + +// Test that uninstalling a temporary addon can be canceled +add_task(async function cancelUninstallTemporary() { + await AddonManager.installTemporaryAddon(XPIS.test_undouninstall1); + + let a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + Assert.notEqual(a1, null); + BootstrapMonitor.checkInstalled(ID, "1.0"); + BootstrapMonitor.checkStarted(ID, "1.0"); + Assert.equal(getInstallReason(ID), ADDON_INSTALL); + Assert.equal(getStartupReason(ID), ADDON_INSTALL); + Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE); + Assert.ok(a1.isActive); + Assert.ok(!a1.userDisabled); + + await expectEvents( + { + addonEvents: { + "undouninstall1@tests.mozilla.org": [{ event: "onUninstalling" }], + }, + }, + () => a1.uninstall(true) + ); + + BootstrapMonitor.checkNotStarted(ID, "1.0"); + Assert.ok(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations)); + + let promises = [ + promiseAddonEvent("onOperationCancelled"), + promiseWebExtensionStartup("undouninstall1@tests.mozilla.org"), + ]; + a1.cancelUninstall(); + await Promise.all(promises); + + a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org"); + + Assert.notEqual(a1, null); + BootstrapMonitor.checkStarted(ID, "1.0"); + Assert.equal(a1.pendingOperations, 0); + + await promiseRestartManager(); +}); + +// Tests that cancelling the uninstall of an incompatible restartless addon +// does not start the addon +add_task(async function cancelUninstallIncompatibleRestartless() { + await promiseInstallFile(XPIS.test_undoincompatible); + + let a1 = await promiseAddonByID(INCOMPAT_ID); + Assert.notEqual(a1, null); + BootstrapMonitor.checkNotStarted(INCOMPAT_ID); + Assert.ok(!a1.isActive); + + await expectEvents( + { + addonEvents: { + "incompatible@tests.mozilla.org": [{ event: "onUninstalling" }], + }, + }, + () => a1.uninstall(true) + ); + + a1 = await promiseAddonByID(INCOMPAT_ID); + Assert.notEqual(a1, null); + Assert.ok(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations)); + Assert.ok(!a1.isActive); + + await expectEvents( + { + addonEvents: { + "incompatible@tests.mozilla.org": [{ event: "onOperationCancelled" }], + }, + }, + async () => { + a1.cancelUninstall(); + } + ); + + a1 = await promiseAddonByID(INCOMPAT_ID); + Assert.notEqual(a1, null); + BootstrapMonitor.checkNotStarted(INCOMPAT_ID); + Assert.equal(a1.pendingOperations, 0); + Assert.ok(!a1.isActive); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update.js b/toolkit/mozapps/extensions/test/xpcshell/test_update.js new file mode 100644 index 0000000000..1bd41e8d71 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_update.js @@ -0,0 +1,834 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies that add-on update checks work + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); +// This test uses add-on versions that follow the toolkit version but we +// started to encourage the use of a simpler format in Bug 1793925. We disable +// the pref below to avoid install errors. +Services.prefs.setBoolPref( + "extensions.webextensions.warnings-as-errors", + false +); + +const updateFile = "test_update.json"; + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +const ADDONS = { + test_update: { + id: "addon1@tests.mozilla.org", + version: "2.0", + name: "Test 1", + }, + test_update8: { + id: "addon8@tests.mozilla.org", + version: "2.0", + name: "Test 8", + }, + test_update12: { + id: "addon12@tests.mozilla.org", + version: "2.0", + name: "Test 12", + }, + test_install2_1: { + id: "addon2@tests.mozilla.org", + version: "2.0", + name: "Real Test 2", + }, + test_install2_2: { + id: "addon2@tests.mozilla.org", + version: "3.0", + name: "Real Test 3", + }, +}; + +var testserver = createHttpServer({ hosts: ["example.com"] }); +testserver.registerDirectory("/data/", do_get_file("data")); + +const XPIS = {}; + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + + Services.locale.requestedLocales = ["fr-FR"]; + + for (let [name, info] of Object.entries(ADDONS)) { + XPIS[name] = createTempWebExtensionFile({ + manifest: { + name: info.name, + version: info.version, + browser_specific_settings: { gecko: { id: info.id } }, + }, + }); + testserver.registerFile(`/addons/${name}.xpi`, XPIS[name]); + } + + AddonTestUtils.updateReason = AddonManager.UPDATE_WHEN_USER_REQUESTED; + + await promiseStartupManager(); +}); + +// Verify that an update is available and can be installed. +add_task(async function test_apply_update() { + await promiseInstallWebExtension({ + manifest: { + name: "Test Addon 1", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "addon1@tests.mozilla.org", + update_url: `http://example.com/data/${updateFile}`, + }, + }, + }, + }); + + let a1 = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + notEqual(a1, null); + equal(a1.version, "1.0"); + equal(a1.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DEFAULT); + equal(a1.releaseNotesURI, null); + notEqual(a1.syncGUID, null); + + let originalSyncGUID = a1.syncGUID; + + await expectEvents( + { + ignorePlugins: true, + addonEvents: { + "addon1@tests.mozilla.org": [ + { + event: "onPropertyChanged", + properties: ["applyBackgroundUpdates"], + }, + ], + }, + }, + async () => { + a1.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE; + } + ); + + a1.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE; + + let install; + await expectEvents( + { + ignorePlugins: true, + installEvents: [{ event: "onNewInstall" }], + }, + async () => { + ({ updateAvailable: install } = + await AddonTestUtils.promiseFindAddonUpdates(a1)); + } + ); + + let installs = await AddonManager.getAllInstalls(); + equal(installs.length, 1); + equal(installs[0], install); + + equal(install.name, a1.name); + equal(install.version, "2.0"); + equal(install.state, AddonManager.STATE_AVAILABLE); + equal(install.existingAddon, a1); + equal(install.releaseNotesURI.spec, "http://example.com/updateInfo.xhtml"); + + // Verify that another update check returns the same AddonInstall + let { updateAvailable: install2 } = + await AddonTestUtils.promiseFindAddonUpdates(a1); + + installs = await AddonManager.getAllInstalls(); + equal(installs.length, 1); + equal(installs[0], install); + equal(install2, install); + + await expectEvents( + { + ignorePlugins: true, + installEvents: [ + { event: "onDownloadStarted" }, + { event: "onDownloadEnded", returnValue: false }, + ], + }, + () => { + install.install(); + } + ); + + equal(install.state, AddonManager.STATE_DOWNLOADED); + + // Continue installing the update. + // Verify that another update check returns no new update + let { updateAvailable } = await AddonTestUtils.promiseFindAddonUpdates( + install.existingAddon + ); + + ok( + !updateAvailable, + "Should find no available updates when one is already downloading" + ); + + installs = await AddonManager.getAllInstalls(); + equal(installs.length, 1); + equal(installs[0], install); + + await expectEvents( + { + ignorePlugins: true, + addonEvents: { + "addon1@tests.mozilla.org": [ + { event: "onInstalling" }, + { event: "onInstalled" }, + ], + }, + installEvents: [ + { event: "onInstallStarted" }, + { event: "onInstallEnded" }, + ], + }, + () => { + install.install(); + } + ); + + await AddonTestUtils.loadAddonsList(true); + + // Grab the current time so we can check the mtime of the add-on below + // without worrying too much about how long other tests take. + let startupTime = Date.now(); + + ok(isExtensionInBootstrappedList(profileDir, "addon1@tests.mozilla.org")); + + a1 = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + notEqual(a1, null); + equal(a1.version, "2.0"); + ok(isExtensionInBootstrappedList(profileDir, a1.id)); + equal(a1.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DISABLE); + equal(a1.releaseNotesURI.spec, "http://example.com/updateInfo.xhtml"); + notEqual(a1.syncGUID, null); + equal(originalSyncGUID, a1.syncGUID); + + // Make sure that the extension lastModifiedTime was updated. + let testFile = getAddonFile(a1); + let difference = testFile.lastModifiedTime - startupTime; + Assert.less(Math.abs(difference), MAX_TIME_DIFFERENCE); + + await a1.uninstall(); +}); + +// Check that an update check finds compatibility updates and applies them +add_task(async function test_compat_update() { + await promiseInstallWebExtension({ + manifest: { + name: "Test Addon 2", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "addon2@tests.mozilla.org", + update_url: "http://example.com/data/" + updateFile, + strict_max_version: "0", + }, + }, + }, + }); + + let a2 = await AddonManager.getAddonByID("addon2@tests.mozilla.org"); + notEqual(a2, null); + ok(a2.isActive); + ok(a2.isCompatible); + ok(!a2.appDisabled); + ok(a2.isCompatibleWith("0", "0")); + + let result = await AddonTestUtils.promiseFindAddonUpdates(a2); + ok(result.compatibilityUpdate, "Should have seen a compatibility update"); + ok(!result.updateAvailable, "Should not have seen a version update"); + + ok(a2.isCompatible); + ok(!a2.appDisabled); + ok(a2.isActive); + + await promiseRestartManager(); + + a2 = await AddonManager.getAddonByID("addon2@tests.mozilla.org"); + notEqual(a2, null); + ok(a2.isActive); + ok(a2.isCompatible); + ok(!a2.appDisabled); + await a2.uninstall(); +}); + +// Checks that we see no compatibility information when there is none. +add_task(async function test_no_compat() { + gAppInfo.platformVersion = "5"; + await promiseRestartManager("5"); + await promiseInstallWebExtension({ + manifest: { + name: "Test Addon 3", + browser_specific_settings: { + gecko: { + id: "addon3@tests.mozilla.org", + update_url: `http://example.com/data/${updateFile}`, + strict_min_version: "5", + }, + }, + }, + }); + + gAppInfo.platformVersion = "1"; + await promiseRestartManager("1"); + + let a3 = await AddonManager.getAddonByID("addon3@tests.mozilla.org"); + notEqual(a3, null); + ok(!a3.isActive); + ok(!a3.isCompatible); + ok(a3.appDisabled); + ok(a3.isCompatibleWith("5", "5")); + ok(!a3.isCompatibleWith("2", "2")); + + let result = await AddonTestUtils.promiseFindAddonUpdates(a3); + ok( + !result.compatibilityUpdate, + "Should not have seen a compatibility update" + ); + ok(!result.updateAvailable, "Should not have seen a version update"); +}); + +// Checks that compatibility info for future apps are detected but don't make +// the item compatibile. +add_task(async function test_future_compat() { + let a3 = await AddonManager.getAddonByID("addon3@tests.mozilla.org"); + notEqual(a3, null); + ok(!a3.isActive); + ok(!a3.isCompatible); + ok(a3.appDisabled); + ok(a3.isCompatibleWith("5", "5")); + ok(!a3.isCompatibleWith("2", "2")); + + let result = await AddonTestUtils.promiseFindAddonUpdates( + a3, + undefined, + "3.0", + "3.0" + ); + ok(result.compatibilityUpdate, "Should have seen a compatibility update"); + ok(!result.updateAvailable, "Should not have seen a version update"); + + ok(!a3.isActive); + ok(!a3.isCompatible); + ok(a3.appDisabled); + + await promiseRestartManager(); + + a3 = await AddonManager.getAddonByID("addon3@tests.mozilla.org"); + notEqual(a3, null); + ok(!a3.isActive); + ok(!a3.isCompatible); + ok(a3.appDisabled); + + await a3.uninstall(); +}); + +// Test that background update checks work +add_task(async function test_background_update() { + await promiseInstallWebExtension({ + manifest: { + name: "Test Addon 1", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "addon1@tests.mozilla.org", + update_url: `http://example.com/data/${updateFile}`, + strict_min_version: "1", + strict_max_version: "1", + }, + }, + }, + }); + + function checkInstall(install) { + notEqual(install.existingAddon, null); + equal(install.existingAddon.id, "addon1@tests.mozilla.org"); + } + + await expectEvents( + { + ignorePlugins: true, + addonEvents: { + "addon1@tests.mozilla.org": [ + { event: "onInstalling" }, + { event: "onInstalled" }, + ], + }, + installEvents: [ + { event: "onNewInstall" }, + { event: "onDownloadStarted" }, + { event: "onDownloadEnded", callback: checkInstall }, + { event: "onInstallStarted" }, + { event: "onInstallEnded" }, + ], + }, + () => { + AddonManagerPrivate.backgroundUpdateCheck(); + } + ); + + let a1 = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); + notEqual(a1, null); + equal(a1.version, "2.0"); + equal(a1.releaseNotesURI.spec, "http://example.com/updateInfo.xhtml"); + + await a1.uninstall(); +}); + +const STATE_BLOCKED = Ci.nsIBlocklistService.STATE_BLOCKED; + +const PARAMS = + "?" + + [ + "req_version=%REQ_VERSION%", + "item_id=%ITEM_ID%", + "item_version=%ITEM_VERSION%", + "item_maxappversion=%ITEM_MAXAPPVERSION%", + "item_status=%ITEM_STATUS%", + "app_id=%APP_ID%", + "app_version=%APP_VERSION%", + "current_app_version=%CURRENT_APP_VERSION%", + "app_os=%APP_OS%", + "app_abi=%APP_ABI%", + "app_locale=%APP_LOCALE%", + "update_type=%UPDATE_TYPE%", + ].join("&"); + +const PARAM_ADDONS = { + "addon1@tests.mozilla.org": { + manifest: { + name: "Test Addon 1", + version: "5.0", + browser_specific_settings: { + gecko: { + id: "addon1@tests.mozilla.org", + update_url: `http://example.com/data/param_test.json${PARAMS}`, + strict_min_version: "1", + strict_max_version: "2", + }, + }, + }, + params: { + item_version: "5.0", + item_maxappversion: "2", + item_status: "userEnabled", + app_version: "1", + update_type: "97", + }, + updateType: [AddonManager.UPDATE_WHEN_USER_REQUESTED], + }, + + "addon2@tests.mozilla.org": { + manifest: { + name: "Test Addon 2", + version: "67.0.5b1", + browser_specific_settings: { + gecko: { + id: "addon2@tests.mozilla.org", + update_url: "http://example.com/data/param_test.json" + PARAMS, + strict_min_version: "0", + strict_max_version: "3", + }, + }, + }, + initialState: { + userDisabled: true, + }, + params: { + item_version: "67.0.5b1", + item_maxappversion: "3", + item_status: "userDisabled", + app_version: "1", + update_type: "49", + }, + updateType: [AddonManager.UPDATE_WHEN_ADDON_INSTALLED], + compatOnly: true, + }, + + "addon3@tests.mozilla.org": { + manifest: { + name: "Test Addon 3", + version: "1.3+", + browser_specific_settings: { + gecko: { + id: "addon3@tests.mozilla.org", + update_url: `http://example.com/data/param_test.json${PARAMS}`, + }, + }, + }, + params: { + item_version: "1.3+", + item_status: "userEnabled", + app_version: "1", + update_type: "112", + }, + updateType: [AddonManager.UPDATE_WHEN_PERIODIC_UPDATE], + }, + + "addon4@tests.mozilla.org": { + manifest: { + name: "Test Addon 4", + version: "0.5ab6", + browser_specific_settings: { + gecko: { + id: "addon4@tests.mozilla.org", + update_url: `http://example.com/data/param_test.json${PARAMS}`, + strict_min_version: "1", + strict_max_version: "5", + }, + }, + }, + params: { + item_version: "0.5ab6", + item_maxappversion: "5", + item_status: "userEnabled", + app_version: "2", + update_type: "98", + }, + updateType: [AddonManager.UPDATE_WHEN_NEW_APP_DETECTED, "2"], + }, + + "addon5@tests.mozilla.org": { + manifest: { + name: "Test Addon 5", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "addon5@tests.mozilla.org", + update_url: `http://example.com/data/param_test.json${PARAMS}`, + strict_min_version: "1", + strict_max_version: "1", + }, + }, + }, + params: { + item_version: "1.0", + item_maxappversion: "1", + item_status: "userEnabled", + app_version: "1", + update_type: "35", + }, + updateType: [AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED], + compatOnly: true, + }, + + "addon6@tests.mozilla.org": { + manifest: { + name: "Test Addon 6", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "addon6@tests.mozilla.org", + update_url: `http://example.com/data/param_test.json${PARAMS}`, + strict_min_version: "1", + strict_max_version: "1", + }, + }, + }, + params: { + item_version: "1.0", + item_maxappversion: "1", + item_status: "userEnabled", + app_version: "1", + update_type: "99", + }, + updateType: [AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED], + }, + + "blocklist2@tests.mozilla.org": { + manifest: { + name: "Test Addon 1", + version: "5.0", + browser_specific_settings: { + gecko: { + id: "blocklist2@tests.mozilla.org", + update_url: `http://example.com/data/param_test.json${PARAMS}`, + strict_min_version: "1", + strict_max_version: "2", + }, + }, + }, + params: { + item_version: "5.0", + item_maxappversion: "2", + item_status: "userEnabled,blocklisted", + app_version: "1", + update_type: "97", + }, + updateType: [AddonManager.UPDATE_WHEN_USER_REQUESTED], + blocklistState: STATE_BLOCKED, + }, +}; + +const PARAM_IDS = Object.keys(PARAM_ADDONS); + +// Verify the parameter escaping in update urls. +add_task(async function test_params() { + let blocked = []; + for (let [id, options] of Object.entries(PARAM_ADDONS)) { + if (options.blocklistState == STATE_BLOCKED) { + blocked.push(`${id}:${options.manifest.version}`); + } + } + let extensionsMLBF = [{ stash: { blocked, unblocked: [] }, stash_time: 0 }]; + await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF }); + + for (let [id, options] of Object.entries(PARAM_ADDONS)) { + await promiseInstallWebExtension({ manifest: options.manifest }); + + if (options.initialState) { + let addon = await AddonManager.getAddonByID(id); + await setInitialState(addon, options.initialState); + } + } + + let resultsPromise = new Promise(resolve => { + let results = new Map(); + + testserver.registerPathHandler( + "/data/param_test.json", + function (request, response) { + let params = new URLSearchParams(request.queryString); + let itemId = params.get("item_id"); + ok( + !results.has(itemId), + `Should not see a duplicate request for item ${itemId}` + ); + + results.set(itemId, params); + + if (results.size === PARAM_IDS.length) { + resolve(results); + } + + request.setStatusLine(null, 500, "Server Error"); + } + ); + }); + + let addons = await getAddons(PARAM_IDS); + for (let [id, options] of Object.entries(PARAM_ADDONS)) { + // Having an onUpdateAvailable callback in the listener automagically adds + // UPDATE_TYPE_NEWVERSION to the update type flags in the request. + let listener = options.compatOnly ? {} : { onUpdateAvailable() {} }; + + addons.get(id).findUpdates(listener, ...options.updateType); + } + + let baseParams = { + req_version: "2", + app_id: "xpcshell@tests.mozilla.org", + current_app_version: "1", + app_os: "XPCShell", + app_abi: "noarch-spidermonkey", + app_locale: "fr-FR", + }; + + let results = await resultsPromise; + for (let [id, options] of Object.entries(PARAM_ADDONS)) { + info(`Checking update params for ${id}`); + + let expected = Object.assign({}, baseParams, options.params); + let params = results.get(id); + + for (let [prop, value] of Object.entries(expected)) { + equal(params.get(prop), value, `Expected value for ${prop}`); + } + } + + for (let [, addon] of await getAddons(PARAM_IDS)) { + await addon.uninstall(); + } +}); + +// Tests that if a manifest claims compatibility then the add-on will be +// seen as compatible regardless of what the update payload says. +add_task(async function test_manifest_compat() { + await promiseInstallWebExtension({ + manifest: { + name: "Test Addon 1", + version: "5.0", + browser_specific_settings: { + gecko: { + id: "addon4@tests.mozilla.org", + update_url: `http://example.com/data/${updateFile}`, + strict_min_version: "0", + strict_max_version: "1", + }, + }, + }, + }); + + let a4 = await AddonManager.getAddonByID("addon4@tests.mozilla.org"); + ok(a4.isActive, "addon4 is active"); + ok(a4.isCompatible, "addon4 is compatible"); + + // Test that a normal update check won't decrease a targetApplication's + // maxVersion but an update check for a new application will. + await AddonTestUtils.promiseFindAddonUpdates( + a4, + AddonManager.UPDATE_WHEN_PERIODIC_UPDATE + ); + ok(a4.isActive, "addon4 is active"); + ok(a4.isCompatible, "addon4 is compatible"); + + await AddonTestUtils.promiseFindAddonUpdates( + a4, + AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED + ); + ok(!a4.isActive, "addon4 is not active"); + ok(!a4.isCompatible, "addon4 is not compatible"); + + await a4.uninstall(); +}); + +// Test that the background update check doesn't update an add-on that isn't +// allowed to update automatically. +add_task(async function test_no_auto_update() { + // Have an add-on there that will be updated so we see some events from it + await promiseInstallWebExtension({ + manifest: { + name: "Test Addon 1", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "addon1@tests.mozilla.org", + update_url: `http://example.com/data/${updateFile}`, + }, + }, + }, + }); + + await promiseInstallWebExtension({ + manifest: { + name: "Test Addon 8", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "addon8@tests.mozilla.org", + update_url: `http://example.com/data/${updateFile}`, + }, + }, + }, + }); + + let a8 = await AddonManager.getAddonByID("addon8@tests.mozilla.org"); + a8.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE; + + // The background update check will find updates for both add-ons but only + // proceed to install one of them. + let listener; + await new Promise(resolve => { + listener = { + onNewInstall(aInstall) { + let id = aInstall.existingAddon.id; + ok( + id == "addon1@tests.mozilla.org" || id == "addon8@tests.mozilla.org", + "Saw unexpected onNewInstall for " + id + ); + }, + + onDownloadStarted(aInstall) { + equal(aInstall.existingAddon.id, "addon1@tests.mozilla.org"); + }, + + onDownloadEnded(aInstall) { + equal(aInstall.existingAddon.id, "addon1@tests.mozilla.org"); + }, + + onDownloadFailed(aInstall) { + ok(false, "Should not have seen onDownloadFailed event"); + }, + + onDownloadCancelled(aInstall) { + ok(false, "Should not have seen onDownloadCancelled event"); + }, + + onInstallStarted(aInstall) { + equal(aInstall.existingAddon.id, "addon1@tests.mozilla.org"); + }, + + onInstallEnded(aInstall) { + equal(aInstall.existingAddon.id, "addon1@tests.mozilla.org"); + + resolve(); + }, + + onInstallFailed(aInstall) { + ok(false, "Should not have seen onInstallFailed event"); + }, + + onInstallCancelled(aInstall) { + ok(false, "Should not have seen onInstallCancelled event"); + }, + }; + AddonManager.addInstallListener(listener); + AddonManagerPrivate.backgroundUpdateCheck(); + }); + AddonManager.removeInstallListener(listener); + + let a1; + [a1, a8] = await AddonManager.getAddonsByIDs([ + "addon1@tests.mozilla.org", + "addon8@tests.mozilla.org", + ]); + notEqual(a1, null); + equal(a1.version, "2.0"); + await a1.uninstall(); + + notEqual(a8, null); + equal(a8.version, "1.0"); + await a8.uninstall(); +}); + +// Test that the update check returns nothing for addons in locked install +// locations. +add_task(async function run_test_locked_install() { + const lockedDir = gProfD.clone(); + lockedDir.append("locked_extensions"); + registerDirectory("XREAppFeat", lockedDir); + + await promiseShutdownManager(); + + let xpi = await createTempWebExtensionFile({ + manifest: { + name: "Test Addon 13", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "addon13@tests.mozilla.org", + update_url: "http://example.com/data/test_update.json", + }, + }, + }, + }); + xpi.copyTo(lockedDir, "addon13@tests.mozilla.org.xpi"); + + let validAddons = { system: ["addon13@tests.mozilla.org"] }; + await overrideBuiltIns(validAddons); + + await promiseStartupManager(); + + let a13 = await AddonManager.getAddonByID("addon13@tests.mozilla.org"); + notEqual(a13, null); + + let result = await AddonTestUtils.promiseFindAddonUpdates(a13); + ok( + !result.compatibilityUpdate, + "Should not have seen a compatibility update" + ); + ok(!result.updateAvailable, "Should not have seen a version update"); + + let installs = await AddonManager.getAllInstalls(); + equal(installs.length, 0); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_updateCancel.js b/toolkit/mozapps/extensions/test/xpcshell/test_updateCancel.js new file mode 100644 index 0000000000..ac201f434c --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_updateCancel.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test cancelling add-on update checks while in progress (bug 925389) + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + +// Install one extension +// Start download of update check (but delay HTTP response) +// Cancel update check +// - ensure we get cancel notification +// complete HTTP response +// - ensure no callbacks after cancel +// - ensure update is gone + +// Create an addon update listener containing a promise +// that resolves when the update is cancelled +function makeCancelListener() { + let resolve, reject; + let promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + + return { + onUpdateAvailable(addon, install) { + reject("Should not have seen onUpdateAvailable notification"); + }, + + onUpdateFinished(aAddon, aError) { + info("onUpdateCheckFinished: " + aAddon.id + " " + aError); + resolve(aError); + }, + promise, + }; +} + +let testserver = createHttpServer({ hosts: ["example.com"] }); + +// Set up the HTTP server so that we can control when it responds +let _httpResolve; +function resetUpdateListener() { + return new Promise(resolve => { + _httpResolve = resolve; + }); +} + +testserver.registerPathHandler("/data/test_update.json", (req, resp) => { + resp.processAsync(); + _httpResolve([req, resp]); +}); + +const UPDATE_RESPONSE = { + addons: { + "addon1@tests.mozilla.org": { + updates: [ + { + version: "2.0", + update_link: "http://example.com/addons/test_update.xpi", + applications: { + gecko: { + strict_min_version: "1", + strict_max_version: "1", + }, + }, + }, + ], + }, + }, +}; + +add_task(async function cancel_during_check() { + await promiseStartupManager(); + + await promiseInstallWebExtension({ + manifest: { + name: "Test Addon 1", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "addon1@tests.mozilla.org", + update_url: "http://example.com/data/test_update.json", + }, + }, + }, + }); + + let a1 = await promiseAddonByID("addon1@tests.mozilla.org"); + Assert.notEqual(a1, null); + + let requestPromise = resetUpdateListener(); + let listener = makeCancelListener(); + a1.findUpdates(listener, AddonManager.UPDATE_WHEN_USER_REQUESTED); + + // Wait for the http request to arrive + let [, /* request */ response] = await requestPromise; + + // cancelUpdate returns true if there is an update check in progress + Assert.ok(a1.cancelUpdate()); + + let updateResult = await listener.promise; + Assert.equal(AddonManager.UPDATE_STATUS_CANCELLED, updateResult); + + // Now complete the HTTP request + response.write(JSON.stringify(UPDATE_RESPONSE)); + response.finish(); + + // trying to cancel again should return false, i.e. nothing to cancel + Assert.ok(!a1.cancelUpdate()); +}); + +// Test that update check is cancelled if the XPI provider shuts down while +// the update check is in progress +add_task(async function shutdown_during_check() { + // Reset our HTTP listener + let requestPromise = resetUpdateListener(); + + let a1 = await promiseAddonByID("addon1@tests.mozilla.org"); + Assert.notEqual(a1, null); + + let listener = makeCancelListener(); + a1.findUpdates(listener, AddonManager.UPDATE_WHEN_USER_REQUESTED); + + // Wait for the http request to arrive + let [, /* request */ response] = await requestPromise; + + await promiseShutdownManager(); + + let updateResult = await listener.promise; + Assert.equal(AddonManager.UPDATE_STATUS_CANCELLED, updateResult); + + // Now complete the HTTP request + response.write(JSON.stringify(UPDATE_RESPONSE)); + response.finish(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update_addontype.js b/toolkit/mozapps/extensions/test/xpcshell/test_update_addontype.js new file mode 100644 index 0000000000..ca324cf4ef --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_addontype.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false); + +let server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); + +add_task(async function setup() { + await ExtensionTestUtils.startAddonManager(); +}); + +add_task(async function test_update_theme_to_extension() { + const THEME_ID = "theme@tests.mozilla.org"; + await promiseInstallWebExtension({ + manifest: { + version: "1.0", + theme: {}, + browser_specific_settings: { + gecko: { + id: THEME_ID, + update_url: "http://example.com/update.json", + }, + }, + }, + }); + + let xpi = await createTempWebExtensionFile({ + manifest: { + version: "2.0", + browser_specific_settings: { gecko: { id: THEME_ID } }, + }, + }); + + server.registerFile("/addon.xpi", xpi); + AddonTestUtils.registerJSON(server, "/update.json", { + addons: { + [THEME_ID]: { + updates: [ + { + version: "2.0", + update_link: "http://example.com/addon.xpi", + }, + ], + }, + }, + }); + + let addon = await promiseAddonByID(THEME_ID); + Assert.notEqual(addon, null); + Assert.equal(addon.type, "theme"); + Assert.equal(addon.version, "1.0"); + + let update = await promiseFindAddonUpdates( + addon, + AddonManager.UPDATE_WHEN_USER_REQUESTED + ); + let install = update.updateAvailable; + Assert.notEqual(install, null, "Found available update"); + // Although the downloaded xpi is an "extension", install.type is "theme" + // because install.type reflects the type of the add-on that is being updated. + Assert.equal(install.type, "theme"); + Assert.equal(install.version, "2.0"); + Assert.equal(install.state, AddonManager.STATE_AVAILABLE); + Assert.equal(install.existingAddon, addon); + + await Assert.rejects( + install.install(), + err => install.error == AddonManager.ERROR_UNEXPECTED_ADDON_TYPE, + "Refusing to change addon type from theme to extension" + ); + + await addon.uninstall(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update_compatmode.js b/toolkit/mozapps/extensions/test/xpcshell/test_update_compatmode.js new file mode 100644 index 0000000000..85ec556f95 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_compatmode.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies that add-on update check correctly fills in the +// %COMPATIBILITY_MODE% token in the update URL. + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); + +let testserver = createHttpServer({ hosts: ["example.com"] }); + +let lastMode; +testserver.registerPathHandler("/update.json", (request, response) => { + let params = new URLSearchParams(request.queryString); + lastMode = params.get("mode"); + + response.setHeader("content-type", "application/json", true); + response.write(JSON.stringify({ addons: {} })); +}); + +const ID_NORMAL = "compatmode@tests.mozilla.org"; +const ID_STRICT = "compatmode-strict@tests.mozilla.org"; + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + + let xpi = await createAddon({ + id: ID_NORMAL, + updateURL: "http://example.com/update.json?mode=%COMPATIBILITY_MODE%", + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1", + }, + ], + }); + await manuallyInstall(xpi, AddonTestUtils.profileExtensions, ID_NORMAL); + + xpi = await createAddon({ + id: ID_STRICT, + updateURL: "http://example.com/update.json?mode=%COMPATIBILITY_MODE%", + strictCompatibility: true, + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1", + }, + ], + }); + await manuallyInstall(xpi, AddonTestUtils.profileExtensions, ID_STRICT); + + await promiseStartupManager(); +}); + +// Strict compatibility checking disabled. +add_task(async function test_strict_disabled() { + Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false); + let addon = await AddonManager.getAddonByID(ID_NORMAL); + Assert.notEqual(addon, null); + + await promiseFindAddonUpdates(addon, AddonManager.UPDATE_WHEN_USER_REQUESTED); + Assert.equal( + lastMode, + "normal", + "COMPATIBIILITY_MODE normal was set correctly" + ); +}); + +// Strict compatibility checking enabled. +add_task(async function test_strict_enabled() { + Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, true); + let addon = await AddonManager.getAddonByID(ID_NORMAL); + Assert.notEqual(addon, null); + + await promiseFindAddonUpdates(addon, AddonManager.UPDATE_WHEN_USER_REQUESTED); + Assert.equal( + lastMode, + "strict", + "COMPATIBILITY_MODE strict was set correctly" + ); +}); + +// Strict compatibility checking opt-in. +add_task(async function test_strict_optin() { + Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false); + let addon = await AddonManager.getAddonByID(ID_STRICT); + Assert.notEqual(addon, null); + + await promiseFindAddonUpdates(addon, AddonManager.UPDATE_WHEN_USER_REQUESTED); + Assert.equal( + lastMode, + "normal", + "COMPATIBILITY_MODE is normal even for an addon with strictCompatibility" + ); +}); + +// Compatibility checking disabled. +add_task(async function test_compat_disabled() { + AddonManager.checkCompatibility = false; + let addon = await AddonManager.getAddonByID(ID_NORMAL); + Assert.notEqual(addon, null); + + await promiseFindAddonUpdates(addon, AddonManager.UPDATE_WHEN_USER_REQUESTED); + Assert.equal( + lastMode, + "ignore", + "COMPATIBILITY_MODE ignore was set correctly" + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update_ignorecompat.js b/toolkit/mozapps/extensions/test/xpcshell/test_update_ignorecompat.js new file mode 100644 index 0000000000..7ac01afc53 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_ignorecompat.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This test is disabled but is being kept around so it can eventualy +// be modernized and re-enabled. But is uses obsolete test helpers that +// fail lint, so just skip linting it for now. +/* eslint-disable */ + +// This verifies that add-on update checks work correctly when compatibility +// check is disabled. + +const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled"; + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); +Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false); + +var testserver = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); +testserver.registerDirectory("/data/", do_get_file("data")); +testserver.registerDirectory("/data/", do_get_file("data")); + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + +const updateFile = "test_update.json"; +const appId = "toolkit@mozilla.org"; + +// Test that the update check correctly observes the +// extensions.strictCompatibility pref. +add_test(async function () { + await promiseWriteInstallRDFForExtension( + { + id: "addon9@tests.mozilla.org", + version: "1.0", + updateURL: "http://example.com/data/" + updateFile, + targetApplications: [ + { + id: appId, + minVersion: "0.1", + maxVersion: "0.2", + }, + ], + name: "Test Addon 9", + }, + profileDir + ); + + await promiseRestartManager(); + + AddonManager.addInstallListener({ + onNewInstall(aInstall) { + if (aInstall.existingAddon.id != "addon9@tests.mozilla.org") + do_throw( + "Saw unexpected onNewInstall for " + aInstall.existingAddon.id + ); + Assert.equal(aInstall.version, "4.0"); + }, + onDownloadFailed(aInstall) { + run_next_test(); + }, + }); + + Services.prefs.setCharPref( + PREF_GETADDONS_BYIDS, + `http://example.com/data/test_update_addons.json` + ); + Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true); + + AddonManagerInternal.backgroundUpdateCheck(); +}); + +// Test that the update check correctly observes when an addon opts-in to +// strict compatibility checking. +add_test(async function () { + await promiseWriteInstallRDFForExtension( + { + id: "addon11@tests.mozilla.org", + version: "1.0", + updateURL: "http://example.com/data/" + updateFile, + targetApplications: [ + { + id: appId, + minVersion: "0.1", + maxVersion: "0.2", + }, + ], + name: "Test Addon 11", + }, + profileDir + ); + + await promiseRestartManager(); + + let a11 = await AddonManager.getAddonByID("addon11@tests.mozilla.org"); + Assert.notEqual(a11, null); + + a11.findUpdates( + { + onCompatibilityUpdateAvailable() { + do_throw("Should not have seen compatibility information"); + }, + + onUpdateAvailable() { + do_throw("Should not have seen an available update"); + }, + + onUpdateFinished() { + run_next_test(); + }, + }, + AddonManager.UPDATE_WHEN_USER_REQUESTED + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update_isPrivileged.js b/toolkit/mozapps/extensions/test/xpcshell/test_update_isPrivileged.js new file mode 100644 index 0000000000..525dbfa25a --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_isPrivileged.js @@ -0,0 +1,181 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +ChromeUtils.defineESModuleGetters(this, { + ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs", +}); + +AddonTestUtils.usePrivilegedSignatures = id => id === "privileged@ext"; + +const EXTENSION_API_IMPL = ` +this.extensionApiImpl = class extends ExtensionAPI { + onStartup() { + extensions.emit("test-ExtensionAPI-onStartup", { + extensionId: this.extension.id, + version: this.extension.manifest.version, + }); + } + static onUpdate(id, manifest) { + extensions.emit("test-ExtensionAPI-onUpdate", { + extensionId: id, + version: manifest.version, + }); + } +};`; + +function setupTestExtensionAPI() { + // The EXTENSION_API_IMPL script is going to be loaded in the main process, + // where only safe loads are permitted. So we generate a resource:-URL, to + // avoid the use of security.allow_parent_unrestricted_js_loads. + let resProto = Services.io + .getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + resProto.setSubstitution( + "extensionApiImplJs", + Services.io.newURI(`data:,${encodeURIComponent(EXTENSION_API_IMPL)}`) + ); + registerCleanupFunction(() => { + resProto.setSubstitution("extensionApiImplJs", null); + }); + + const modules = { + extensionApiImpl: { + url: "resource://extensionApiImplJs", + events: ["startup", "update"], + }, + }; + + Services.catMan.addCategoryEntry( + "webextension-modules", + "test-register-extensionApiImpl", + `data:,${JSON.stringify(modules)}`, + false, + false + ); +} + +async function runInstallAndUpdate({ + extensionId, + expectPrivileged, + installExtensionData, +}) { + let extensionData = { + useAddonManager: "permanent", + manifest: { + browser_specific_settings: { gecko: { id: extensionId } }, + version: "1.1", + }, + }; + let events = []; + function onUpdated(type, params) { + params = { type, ...params }; + // resourceURI cannot be serialized for use with deepEqual. + delete params.resourceURI; + events.push(params); + } + function onExtensionAPI(type, params) { + events.push({ type, ...params }); + } + ExtensionParent.apiManager.on("update", onUpdated); + ExtensionParent.apiManager.on("test-ExtensionAPI-onStartup", onExtensionAPI); + ExtensionParent.apiManager.on("test-ExtensionAPI-onUpdate", onExtensionAPI); + + let { addon } = await installExtensionData(extensionData); + equal(addon.isPrivileged, expectPrivileged, "Expected isPrivileged"); + + extensionData.manifest.version = "2.22"; + extensionData.manifest.permissions = ["mozillaAddons"]; + // May warn about invalid permissions when the extension is not privileged. + ExtensionTestUtils.failOnSchemaWarnings(false); + let extension = await installExtensionData(extensionData); + ExtensionTestUtils.failOnSchemaWarnings(true); + await extension.unload(); + + ExtensionParent.apiManager.off("update", onUpdated); + ExtensionParent.apiManager.off("test-ExtensionAPI-onStartup", onExtensionAPI); + ExtensionParent.apiManager.off("test-ExtensionAPI-onUpdate", onExtensionAPI); + + // Verify that we have (1) installed and (2) updated the extension. + Assert.deepEqual( + events, + [ + { type: "test-ExtensionAPI-onStartup", extensionId, version: "1.1" }, + // The next two events show that ExtensionParent has run the onUpdate + // handler, during which ExtensionData has supposedly been constructed. + { type: "update", id: extensionId, isPrivileged: expectPrivileged }, + { type: "test-ExtensionAPI-onUpdate", extensionId, version: "2.22" }, + { type: "test-ExtensionAPI-onStartup", extensionId, version: "2.22" }, + ], + "Expected startup and update events" + ); +} + +add_task(async function setup() { + setupTestExtensionAPI(); + await ExtensionTestUtils.startAddonManager(); +}); + +// Tests that privileged extensions (e.g builtins) are always parsed with the +// correct isPrivileged flag. +add_task(async function test_install_and_update_builtin() { + let { messages } = await promiseConsoleOutput(async () => { + await runInstallAndUpdate({ + extensionId: "builtin@ext", + expectPrivileged: true, + async installExtensionData(extData) { + return installBuiltinExtension(extData); + }, + }); + }); + + AddonTestUtils.checkMessages(messages, { + expected: [{ message: /Addon with ID builtin@ext already installed,/ }], + forbidden: [{ message: /Invalid extension permission: mozillaAddons/ }], + }); +}); + +add_task(async function test_install_and_update_regular_ext() { + let { messages } = await promiseConsoleOutput(async () => { + await runInstallAndUpdate({ + extensionId: "regular@ext", + expectPrivileged: false, + async installExtensionData(extData) { + let extension = ExtensionTestUtils.loadExtension(extData); + await extension.startup(); + return extension; + }, + }); + }); + let errPattern = + /Loading extension 'regular@ext': Reading manifest: Invalid extension permission: mozillaAddons/; + let permissionWarnings = messages.filter(msg => errPattern.test(msg.message)); + // Expected number of warnings after triggering the update: + // 1. Generated when the loaded by the Addons manager (ExtensionData). + // 2. Generated when read again before ExtensionAPI.onUpdate (ExtensionData). + // 3. Generated when the extension actually runs (Extension). + equal(permissionWarnings.length, 3, "Expected number of permission warnings"); +}); + +add_task(async function test_install_and_update_privileged_ext() { + let { messages } = await promiseConsoleOutput(async () => { + await runInstallAndUpdate({ + extensionId: "privileged@ext", + expectPrivileged: true, + async installExtensionData(extData) { + let extension = ExtensionTestUtils.loadExtension(extData); + await extension.startup(); + return extension; + }, + }); + }); + AddonTestUtils.checkMessages(messages, { + expected: [ + // First installation. + { message: /Starting install of privileged@ext / }, + // Second installation (update). + { message: /Starting install of privileged@ext / }, + ], + forbidden: [{ message: /Invalid extension permission: mozillaAddons/ }], + }); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update_noSystemAddonUpdate.js b/toolkit/mozapps/extensions/test/xpcshell/test_update_noSystemAddonUpdate.js new file mode 100644 index 0000000000..f13187ab33 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_noSystemAddonUpdate.js @@ -0,0 +1,43 @@ +// Tests that system add-on doesnt request update while normal backgroundUpdateCheck + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2"); + +let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); +distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); +registerDirectory("XREAppFeat", distroDir); + +AddonTestUtils.usePrivilegedSignatures = "system"; + +add_task(() => initSystemAddonDirs()); + +const initialSetup = { + async setup() { + await buildPrefilledUpdatesDir(); + distroDir.leafName = "empty"; + }, + initialState: [ + { isUpgrade: false, version: null }, + { isUpgrade: true, version: "2.0" }, + ], +}; + +add_task(async function test_systems_update_uninstall_check() { + await setupSystemAddonConditions(initialSetup, distroDir); + + const testserver = createHttpServer({ hosts: ["example.com"] }); + testserver.registerPathHandler("/update.json", (request, response) => { + Assert.ok( + !request._queryString.includes("system2@tests.mozilla.org"), + "System addon should not request update from normal update process" + ); + }); + + Services.prefs.setCharPref( + "extensions.update.background.url", + "http://example.com/update.json?id=%ITEM_ID%" + ); + + await AddonManagerPrivate.backgroundUpdateCheck(); + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update_strictcompat.js b/toolkit/mozapps/extensions/test/xpcshell/test_update_strictcompat.js new file mode 100644 index 0000000000..14192657dd --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_strictcompat.js @@ -0,0 +1,216 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies that add-on update checks work in conjunction with +// strict compatibility settings. + +const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled"; + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); +Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false); + +const appId = "toolkit@mozilla.org"; + +testserver = createHttpServer({ hosts: ["example.com"] }); +testserver.registerDirectory("/data/", do_get_file("data")); + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + AddonTestUtils.updateReason = AddonManager.UPDATE_WHEN_USER_REQUESTED; + + Services.prefs.setCharPref( + PREF_GETADDONS_BYIDS, + "http://example.com/data/test_update_addons.json" + ); + Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true); +}); + +// Test that the update check correctly observes the +// extensions.strictCompatibility pref. +add_task(async function test_update_strict() { + const ID = "addon9@tests.mozilla.org"; + let xpi = await createAddon({ + id: ID, + updateURL: "http://example.com/update.json", + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "0.1", + maxVersion: "0.2", + }, + ], + }); + await manuallyInstall(xpi, AddonTestUtils.profileExtensions, ID); + + await promiseStartupManager(); + + await AddonRepository.backgroundUpdateCheck(); + + let UPDATE = { + addons: { + [ID]: { + updates: [ + { + version: "2.0", + update_link: "http://example.com/addons/test_update9_2.xpi", + applications: { + gecko: { + strict_min_version: "1", + advisory_max_version: "1", + }, + }, + }, + + // Incompatible when strict compatibility is enabled + { + version: "3.0", + update_link: "http://example.com/addons/test_update9_3.xpi", + applications: { + gecko: { + strict_min_version: "0.9", + advisory_max_version: "0.9", + }, + }, + }, + + // Addon for future version of app + { + version: "4.0", + update_link: "http://example.com/addons/test_update9_5.xpi", + applications: { + gecko: { + strict_min_version: "5", + advisory_max_version: "6", + }, + }, + }, + ], + }, + }, + }; + + AddonTestUtils.registerJSON(testserver, "/update.json", UPDATE); + + let addon = await AddonManager.getAddonByID(ID); + let { updateAvailable } = await promiseFindAddonUpdates(addon); + + Assert.notEqual(updateAvailable, null, "Got update"); + Assert.equal( + updateAvailable.version, + "3.0", + "The correct update was selected" + ); + await addon.uninstall(); + + await promiseShutdownManager(); +}); + +// Tests that compatibility updates are applied to addons when the updated +// compatibility data wouldn't match with strict compatibility enabled. +add_task(async function test_update_strict2() { + const ID = "addon10@tests.mozilla.org"; + let xpi = createAddon({ + id: ID, + updateURL: "http://example.com/update.json", + targetApplications: [ + { + id: appId, + minVersion: "0.1", + maxVersion: "0.2", + }, + ], + }); + await manuallyInstall(xpi, AddonTestUtils.profileExtensions, ID); + + await promiseStartupManager(); + await AddonRepository.backgroundUpdateCheck(); + + const UPDATE = { + addons: { + [ID]: { + updates: [ + { + version: "1.0", + update_link: "http://example.com/addons/test_update10.xpi", + applications: { + gecko: { + strict_min_version: "0.1", + advisory_max_version: "0.4", + }, + }, + }, + ], + }, + }, + }; + + AddonTestUtils.registerJSON(testserver, "/update.json", UPDATE); + + let addon = await AddonManager.getAddonByID(ID); + notEqual(addon, null); + + let result = await promiseFindAddonUpdates(addon); + ok(result.compatibilityUpdate, "Should have seen a compatibility update"); + ok(!result.updateAvailable, "Should not have seen a version update"); + + await addon.uninstall(); + await promiseShutdownManager(); +}); + +// Test that the update check correctly observes when an addon opts-in to +// strict compatibility checking. +add_task(async function test_update_strict_optin() { + const ID = "addon11@tests.mozilla.org"; + let xpi = await createAddon({ + id: ID, + updateURL: "http://example.com/update.json", + targetApplications: [ + { + id: appId, + minVersion: "0.1", + maxVersion: "0.2", + }, + ], + }); + await manuallyInstall(xpi, AddonTestUtils.profileExtensions, ID); + + await promiseStartupManager(); + + await AddonRepository.backgroundUpdateCheck(); + + const UPDATE = { + addons: { + [ID]: { + updates: [ + { + version: "2.0", + update_link: "http://example.com/addons/test_update11.xpi", + applications: { + gecko: { + strict_min_version: "0.1", + strict_max_version: "0.2", + }, + }, + }, + ], + }, + }, + }; + + AddonTestUtils.registerJSON(testserver, "/update.json", UPDATE); + + let addon = await AddonManager.getAddonByID(ID); + notEqual(addon, null); + + let result = await AddonTestUtils.promiseFindAddonUpdates(addon); + ok( + !result.compatibilityUpdate, + "Should not have seen a compatibility update" + ); + ok(!result.updateAvailable, "Should not have seen a version update"); + + await addon.uninstall(); + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update_theme.js b/toolkit/mozapps/extensions/test/xpcshell/test_update_theme.js new file mode 100644 index 0000000000..7d26f23981 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_theme.js @@ -0,0 +1,121 @@ +"use strict"; + +ChromeUtils.defineLazyGetter(this, "Management", () => { + const { ExtensionParent } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionParent.sys.mjs" + ); + return ExtensionParent.apiManager; +}); + +add_task(async function setup() { + let scopes = AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION; + Services.prefs.setIntPref("extensions.enabledScopes", scopes); + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42.0", "42.0"); + + await promiseStartupManager(); +}); + +// Verify that a theme can be updated without issues. +add_task(async function test_update_of_disabled_theme() { + const id = "theme-only@test"; + async function installTheme(version) { + // Upon installing a theme, it is disabled by default. Because of this, + // ExtensionTestUtils.loadExtension cannot be used because it awaits the + // startup of a test extension. + let xpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { gecko: { id } }, + version, + theme: {}, + }, + }); + let install = await AddonManager.getInstallForFile(xpi); + let addon = await install.install(); + ok(addon.userDisabled, "Theme is expected to be disabled by default"); + equal(addon.userPermissions, null, "theme has no userPermissions"); + } + + await installTheme("1.0"); + + let updatePromise = new Promise(resolve => { + Management.on("update", function listener(name, { id: updatedId }) { + Management.off("update", listener); + equal(updatedId, id, "expected theme update"); + resolve(); + }); + }); + await installTheme("2.0"); + await updatePromise; + let addon = await promiseAddonByID(id); + equal(addon.version, "2.0", "Theme should be updated"); + ok(addon.userDisabled, "Theme is still disabled after an update"); + await addon.uninstall(); +}); + +add_task(async function test_builtin_location_migration() { + const ADDON_ID = "mytheme@mozilla.org"; + + let themeDef = { + manifest: { + browser_specific_settings: { gecko: { id: ADDON_ID } }, + version: "1.0", + theme: {}, + }, + }; + + await setupBuiltinExtension(themeDef, "first-loc", false); + await AddonManager.maybeInstallBuiltinAddon( + ADDON_ID, + "1.0", + "resource://first-loc/" + ); + + let addon = await AddonManager.getAddonByID(ADDON_ID); + await addon.enable(); + Assert.ok(!addon.userDisabled, "Add-on should be enabled."); + + Assert.equal( + Services.prefs.getCharPref("extensions.activeThemeID", ""), + ADDON_ID, + "Pref should be set." + ); + + let { addons: activeThemes } = await AddonManager.getActiveAddons(["theme"]); + Assert.equal(activeThemes.length, 1, "Should have 1 theme."); + Assert.equal(activeThemes[0].id, ADDON_ID, "Should have enabled the theme."); + + // If we restart and update, and install a newer version of the theme, + // it should be activated. + await promiseShutdownManager(); + + // Force schema change and restart + Services.prefs.setIntPref("extensions.databaseSchema", 0); + await promiseStartupManager(); + + // Set up a new version of the builtin add-on. + let newDef = { manifest: Object.assign({}, themeDef.manifest) }; + newDef.manifest.version = "1.1"; + await setupBuiltinExtension(newDef, "second-loc"); + await AddonManager.maybeInstallBuiltinAddon( + ADDON_ID, + "1.1", + "resource://second-loc/" + ); + + let newAddon = await AddonManager.getAddonByID(ADDON_ID); + Assert.ok(!newAddon.userDisabled, "Add-on should be enabled."); + + ({ addons: activeThemes } = await AddonManager.getActiveAddons(["theme"])); + Assert.equal(activeThemes.length, 1, "Should still have 1 theme."); + Assert.equal( + activeThemes[0].id, + ADDON_ID, + "Should still have the theme enabled." + ); + Assert.equal( + Services.prefs.getCharPref("extensions.activeThemeID", ""), + ADDON_ID, + "Pref should still be set." + ); + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update_webextensions.js b/toolkit/mozapps/extensions/test/xpcshell/test_update_webextensions.js new file mode 100644 index 0000000000..dbe944013f --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_webextensions.js @@ -0,0 +1,209 @@ +"use strict"; + +// We don't have an easy way to serve update manifests from a secure URL. +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); + +var testserver = createHttpServer(); +gPort = testserver.identity.primaryPort; + +const uuidGenerator = Services.uuid; + +const extensionsDir = gProfD.clone(); +extensionsDir.append("extensions"); + +const addonsDir = gTmpD.clone(); +addonsDir.append("addons"); +addonsDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + +registerCleanupFunction(() => addonsDir.remove(true)); + +testserver.registerDirectory("/addons/", addonsDir); + +let gUpdateManifests = {}; + +function mapManifest(aPath, aManifestData) { + gUpdateManifests[aPath] = aManifestData; + testserver.registerPathHandler(aPath, serveManifest); +} + +function serveManifest(request, response) { + let manifest = gUpdateManifests[request.path]; + + response.setHeader("Content-Type", manifest.contentType, false); + response.write(manifest.data); +} + +async function promiseInstallWebExtension(aData) { + let addonFile = createTempWebExtensionFile(aData); + + let { addon } = await promiseInstallFile(addonFile); + Services.obs.notifyObservers(addonFile, "flush-cache-entry"); + return addon; +} + +var checkUpdates = async function ( + aData, + aReason = AddonManager.UPDATE_WHEN_PERIODIC_UPDATE +) { + function provide(obj, path, value) { + path = path.split("."); + let prop = path.pop(); + + for (let key of path) { + if (!(key in obj)) { + obj[key] = {}; + } + obj = obj[key]; + } + + if (!(prop in obj)) { + obj[prop] = value; + } + } + + let id = uuidGenerator.generateUUID().number; + provide(aData, "addon.id", id); + provide(aData, "addon.manifest.browser_specific_settings.gecko.id", id); + + let updatePath = `/updates/${id}.json`.replace(/[{}]/g, ""); + let updateUrl = `http://localhost:${gPort}${updatePath}`; + + let addonData = { updates: [] }; + let manifestJSON = { + addons: { [id]: addonData }, + }; + + provide( + aData, + "addon.manifest.browser_specific_settings.gecko.update_url", + updateUrl + ); + let awaitInstall = promiseInstallWebExtension(aData.addon); + + for (let version of Object.keys(aData.updates)) { + let update = aData.updates[version]; + update.version = version; + + // Create an add-on manifest based on what's in the current `update`. + provide(update, "addon.id", id); + provide(update, "addon.manifest.browser_specific_settings.gecko.id", id); + let addon = update.addon; + + delete update.addon; + + provide(addon, "manifest.version", version); + let file = createTempWebExtensionFile(addon); + file.moveTo(addonsDir, `${id}-${version}.xpi`.replace(/[{}]/g, "")); + + let path = `/addons/${file.leafName}`; + provide(update, "update_link", `http://localhost:${gPort}${path}`); + + addonData.updates.push(update); + } + + mapManifest(updatePath, { + data: JSON.stringify(manifestJSON), + contentType: "application/json", + }); + + let addon = await awaitInstall; + + let updates = await promiseFindAddonUpdates(addon, aReason); + updates.addon = addon; + + return updates; +}; + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42.0", "42.0"); + + await promiseStartupManager(); + registerCleanupFunction(promiseShutdownManager); +}); + +// Check that compatibility updates are applied. +add_task(async function checkUpdateMetadata() { + let update = await checkUpdates({ + addon: { + manifest: { + version: "1.0", + browser_specific_settings: { gecko: { strict_max_version: "45" } }, + }, + }, + updates: { + "1.0": { + applications: { + gecko: { strict_min_version: "40", strict_max_version: "48" }, + }, + }, + }, + }); + + ok(update.compatibilityUpdate, "have compat update"); + ok(!update.updateAvailable, "have no add-on update"); + + ok(update.addon.isCompatibleWith("40", "40"), "compatible min"); + ok(update.addon.isCompatibleWith("48", "48"), "compatible max"); + ok(!update.addon.isCompatibleWith("49", "49"), "not compatible max"); + + await update.addon.uninstall(); +}); + +// Check that updates from web extensions to web extensions succeed. +add_task(async function checkUpdateToWebExt() { + let update = await checkUpdates({ + addon: { manifest: { version: "1.0" } }, + updates: { + 1.1: {}, + 1.2: {}, + 1.3: { applications: { gecko: { strict_min_version: "48" } } }, + }, + }); + + ok(!update.compatibilityUpdate, "have no compat update"); + ok(update.updateAvailable, "have add-on update"); + + equal(update.addon.version, "1.0", "add-on version"); + + await update.updateAvailable.install(); + + let addon = await promiseAddonByID(update.addon.id); + equal(addon.version, "1.2", "new add-on version"); + + await addon.uninstall(); +}); + +// Check that illegal update URLs are rejected. +add_task(async function checkIllegalUpdateURL() { + const URLS = [ + "chrome://browser/content/", + "data:text/json,...", + "javascript:;", + "/", + ]; + + for (let url of URLS) { + let { messages } = await promiseConsoleOutput(() => { + let addonFile = createTempWebExtensionFile({ + manifest: { browser_specific_settings: { gecko: { update_url: url } } }, + }); + + return AddonManager.getInstallForFile(addonFile).then(install => { + Services.obs.notifyObservers(addonFile, "flush-cache-entry"); + + if (!install || install.state != AddonManager.STATE_DOWNLOAD_FAILED) { + throw new Error("Unexpected state: " + (install && install.state)); + } + }); + }); + + ok( + messages.some(msg => + /Access denied for URL|may not load or link to|is not a valid URL/.test( + msg + ) + ), + "Got checkLoadURI error" + ); + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_updatecheck.js b/toolkit/mozapps/extensions/test/xpcshell/test_updatecheck.js new file mode 100644 index 0000000000..9ddaf82bf3 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_updatecheck.js @@ -0,0 +1,167 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies that AddonUpdateChecker works correctly + +const { AddonUpdateChecker } = ChromeUtils.importESModule( + "resource://gre/modules/addons/AddonUpdateChecker.sys.mjs" +); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + +var testserver = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); + +testserver.registerDirectory("/data/", do_get_file("data")); + +function checkUpdates(aId) { + return new Promise((resolve, reject) => { + AddonUpdateChecker.checkForUpdates( + aId, + `http://example.com/data/test_updatecheck.json`, + { + onUpdateCheckComplete: resolve, + + onUpdateCheckError(status) { + let error = new Error("Update check failed with status " + status); + error.status = status; + reject(error); + }, + } + ); + }); +} + +// Test that a basic update check returns the expected available updates +add_task(async function test_basic_update() { + let updates = await checkUpdates("updatecheck1@tests.mozilla.org"); + + equal(updates.length, 5); + let update = await AddonUpdateChecker.getNewestCompatibleUpdate(updates, {}); + notEqual(update, null); + equal(update.version, "3.0"); + update = AddonUpdateChecker.getCompatibilityUpdate(updates, "2"); + notEqual(update, null); + equal(update.version, "2.0"); + equal(update.targetApplications[0].minVersion, "1"); + equal(update.targetApplications[0].maxVersion, "2"); +}); + +// Test that only newer versions are considered. +add_task(async function test_update_newer_versions_only() { + let updates = await checkUpdates("updatecheck1@tests.mozilla.org"); + + // This should be an AddonWrapper instance, but for the purpose of this test, + // an object with the version property suffices. + let addon = { version: "2.0" }; + let update = await AddonUpdateChecker.getNewestCompatibleUpdate( + updates, + addon + ); + notEqual(update, null); + equal(update.version, "3.0"); + + addon = { version: "3.0" }; + update = await AddonUpdateChecker.getNewestCompatibleUpdate(updates, addon); + equal(update, null); +}); + +/* + * Tests that the security checks are applied correctly + * + * Test updateHash updateLink expected + *-------------------------------------------- + * 4 absent http no update + * 5 sha1 http update + * 6 absent https update + * 7 sha1 https update + * 8 md2 http no update + * 9 md2 https update + */ + +add_task(async function test_4() { + let updates = await checkUpdates("test_bug378216_8@tests.mozilla.org"); + equal(updates.length, 1); + ok(!("updateURL" in updates[0])); +}); + +add_task(async function test_5() { + let updates = await checkUpdates("test_bug378216_9@tests.mozilla.org"); + equal(updates.length, 1); + equal(updates[0].version, "2.0"); + ok("updateURL" in updates[0]); +}); + +add_task(async function test_6() { + let updates = await checkUpdates("test_bug378216_10@tests.mozilla.org"); + equal(updates.length, 1); + equal(updates[0].version, "2.0"); + ok("updateURL" in updates[0]); +}); + +add_task(async function test_7() { + let updates = await checkUpdates("test_bug378216_11@tests.mozilla.org"); + equal(updates.length, 1); + equal(updates[0].version, "2.0"); + ok("updateURL" in updates[0]); +}); + +add_task(async function test_8() { + let updates = await checkUpdates("test_bug378216_12@tests.mozilla.org"); + equal(updates.length, 1); + ok(!("updateURL" in updates[0])); +}); + +add_task(async function test_9() { + let updates = await checkUpdates("test_bug378216_13@tests.mozilla.org"); + equal(updates.length, 1); + equal(updates[0].version, "2.0"); + ok("updateURL" in updates[0]); +}); + +add_task(async function test_no_update_data() { + let updates = await checkUpdates("test_bug378216_14@tests.mozilla.org"); + equal(updates.length, 0); +}); + +add_task(async function test_invalid_json() { + await checkUpdates("test_bug378216_15@tests.mozilla.org") + .then(() => { + ok(false, "Expected the update check to fail"); + }) + .catch(e => { + equal( + e.status, + AddonManager.ERROR_PARSE_ERROR, + "expected AddonManager.ERROR_PARSE_ERROR" + ); + }); +}); + +add_task(async function test_ignore_compat() { + let updates = await checkUpdates("ignore-compat@tests.mozilla.org"); + equal(updates.length, 3); + let update = await AddonUpdateChecker.getNewestCompatibleUpdate( + updates, + {}, // dummy value instead of addon. + null, + null, + true + ); + notEqual(update, null); + equal(update.version, 2); +}); + +add_task(async function test_strict_compat() { + let updates = await checkUpdates("compat-strict-optin@tests.mozilla.org"); + equal(updates.length, 1); + let update = await AddonUpdateChecker.getNewestCompatibleUpdate( + updates, + {}, // dummy value instead of addon. + null, + null, + true, + false + ); + equal(update, null); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_updatecheck_errors.js b/toolkit/mozapps/extensions/test/xpcshell/test_updatecheck_errors.js new file mode 100644 index 0000000000..5bbd723f42 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_updatecheck_errors.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies that add-on update check failures are propagated correctly + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false); + +var testserver; + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + + // Create and configure the HTTP server. + testserver = createHttpServer({ hosts: ["example.com"] }); + testserver.registerDirectory("/data/", do_get_file("data")); + + await promiseStartupManager(); +}); + +// Verify that an update check returns the correct errors. +add_task(async function () { + await promiseInstallWebExtension({ + manifest: { + name: "Test Addon 1", + version: "1.0", + browser_specific_settings: { + gecko: { + id: "addon1@tests.mozilla.org", + update_url: "http://example.com/data/test_missing.json", + }, + }, + }, + }); + + let addon = await promiseAddonByID("addon1@tests.mozilla.org"); + equal(addon.version, "1.0"); + + // We're expecting an error, so resolve when the promise is rejected. + let update = await promiseFindAddonUpdates( + addon, + AddonManager.UPDATE_WHEN_USER_REQUESTED + ).catch(e => e); + + ok(!update.compatibilityUpdate, "not expecting a compatibility update"); + ok(!update.updateAvailable, "not expecting a compatibility update"); + + equal(update.error, AddonManager.UPDATE_STATUS_DOWNLOAD_ERROR); + + await addon.uninstall(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_updatecheck_json.js b/toolkit/mozapps/extensions/test/xpcshell/test_updatecheck_json.js new file mode 100644 index 0000000000..8186ea44a6 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_updatecheck_json.js @@ -0,0 +1,423 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +// This verifies that AddonUpdateChecker works correctly for JSON +// update manifests, particularly for behavior which does not +// cleanly overlap with RDF manifests. + +const TOOLKIT_ID = "toolkit@mozilla.org"; +const TOOLKIT_MINVERSION = "42.0a1"; + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42.0a2", "42.0a2"); + +const { AddonUpdateChecker } = ChromeUtils.importESModule( + "resource://gre/modules/addons/AddonUpdateChecker.sys.mjs" +); + +let testserver = createHttpServer(); +gPort = testserver.identity.primaryPort; + +let gUpdateManifests = {}; + +function mapManifest(aPath, aManifestData) { + gUpdateManifests[aPath] = aManifestData; + testserver.registerPathHandler(aPath, serveManifest); +} + +function serveManifest(request, response) { + let manifest = gUpdateManifests[request.path]; + + response.setHeader("Content-Type", manifest.contentType, false); + response.write(manifest.data); +} + +const extensionsDir = gProfD.clone(); +extensionsDir.append("extensions"); + +function checkUpdates(aData) { + // Registers JSON update manifest for it with the testing server, + // checks for updates, and yields the list of updates on + // success. + + let extension = aData.manifestExtension || "json"; + + let path = `/updates/${aData.id}.${extension}`; + let updateUrl = `http://localhost:${gPort}${path}`; + + let addonData = {}; + if ("updates" in aData) { + addonData.updates = aData.updates; + } + + let manifestJSON = { + addons: { + [aData.id]: addonData, + }, + }; + + mapManifest(path.replace(/\?.*/, ""), { + data: JSON.stringify(manifestJSON), + contentType: aData.contentType || "application/json", + }); + + return new Promise((resolve, reject) => { + AddonUpdateChecker.checkForUpdates(aData.id, updateUrl, { + onUpdateCheckComplete: resolve, + + onUpdateCheckError(status) { + reject(new Error("Update check failed with status " + status)); + }, + }); + }); +} + +add_task(async function test_default_values() { + // Checks that the appropriate defaults are used for omitted values. + + await promiseStartupManager(); + + let updates = await checkUpdates({ + id: "updatecheck-defaults@tests.mozilla.org", + version: "0.1", + updates: [ + { + version: "0.2", + }, + ], + }); + + equal(updates.length, 1); + let update = updates[0]; + + equal(update.targetApplications.length, 2); + let targetApp = update.targetApplications[0]; + + equal(targetApp.id, TOOLKIT_ID); + equal(targetApp.minVersion, TOOLKIT_MINVERSION); + equal(targetApp.maxVersion, "*"); + + equal(update.version, "0.2"); + equal(update.strictCompatibility, false, "inferred strictConpatibility flag"); + equal(update.updateURL, null, "updateURL"); + equal(update.updateHash, null, "updateHash"); + equal(update.updateInfoURL, null, "updateInfoURL"); + + // If there's no applications property, we default to using one + // containing "gecko". If there is an applications property, but + // it doesn't contain "gecko", the update is skipped. + updates = await checkUpdates({ + id: "updatecheck-defaults@tests.mozilla.org", + version: "0.1", + updates: [ + { + version: "0.2", + applications: { foo: {} }, + }, + ], + }); + + equal(updates.length, 0); + + // Updates property is also optional. No updates, but also no error. + updates = await checkUpdates({ + id: "updatecheck-defaults@tests.mozilla.org", + version: "0.1", + }); + + equal(updates.length, 0); +}); + +add_task(async function test_explicit_values() { + // Checks that the appropriate explicit values are used when + // provided. + + let updates = await checkUpdates({ + id: "updatecheck-explicit@tests.mozilla.org", + version: "0.1", + updates: [ + { + version: "0.2", + update_link: "https://example.com/foo.xpi", + update_hash: "sha256:0", + update_info_url: "https://example.com/update_info.html", + applications: { + gecko: { + strict_min_version: "42.0a2.xpcshell", + strict_max_version: "43.xpcshell", + }, + }, + }, + ], + }); + + equal(updates.length, 1); + let update = updates[0]; + + equal(update.targetApplications.length, 2); + let targetApp = update.targetApplications[0]; + + equal(targetApp.id, TOOLKIT_ID); + equal(targetApp.minVersion, "42.0a2.xpcshell"); + equal(targetApp.maxVersion, "43.xpcshell"); + + equal(update.version, "0.2"); + equal(update.strictCompatibility, true, "inferred strictCompatibility flag"); + equal(update.updateURL, "https://example.com/foo.xpi", "updateURL"); + equal(update.updateHash, "sha256:0", "updateHash"); + equal( + update.updateInfoURL, + "https://example.com/update_info.html", + "updateInfoURL" + ); +}); + +add_task(async function test_secure_hashes() { + // Checks that only secure hash functions are accepted for + // non-secure update URLs. + + let hashFunctions = ["sha512", "sha256", "sha1", "md5", "md4", "xxx"]; + + let updateItems = hashFunctions.map((hash, idx) => ({ + version: `0.${idx}`, + update_link: `http://localhost:${gPort}/updates/${idx}-${hash}.xpi`, + update_hash: `${hash}:08ac852190ecd81f40a514ea9299fe9143d9ab5e296b97e73fb2a314de49648a`, + })); + + let { messages, result: updates } = await promiseConsoleOutput(() => { + return checkUpdates({ + id: "updatecheck-hashes@tests.mozilla.org", + version: "0.1", + updates: updateItems, + }); + }); + + equal(updates.length, hashFunctions.length); + + updates = updates.filter(update => update.updateHash || update.updateURL); + equal(updates.length, 2, "expected number of update hashes were accepted"); + + ok(updates[0].updateHash.startsWith("sha512:"), "sha512 hash is present"); + ok(updates[0].updateURL); + + ok(updates[1].updateHash.startsWith("sha256:"), "sha256 hash is present"); + ok(updates[1].updateURL); + + messages = messages.filter(msg => + /Update link.*not secure.*strong enough hash \(needs to be sha256 or sha512\)/.test( + msg.message + ) + ); + equal( + messages.length, + hashFunctions.length - 2, + "insecure hashes generated the expected warning" + ); +}); + +add_task(async function test_strict_compat() { + // Checks that strict compatibility is enabled for strict max + // versions other than "*", but not for advisory max versions. + // Also, ensure that strict max versions take precedence over + // advisory versions. + + let { messages, result: updates } = await promiseConsoleOutput(() => { + return checkUpdates({ + id: "updatecheck-strict@tests.mozilla.org", + version: "0.1", + updates: [ + { + version: "0.2", + applications: { gecko: { strict_max_version: "*" } }, + }, + { + version: "0.3", + applications: { gecko: { strict_max_version: "43" } }, + }, + { + version: "0.4", + applications: { gecko: { advisory_max_version: "43" } }, + }, + { + version: "0.5", + applications: { + gecko: { advisory_max_version: "43", strict_max_version: "44" }, + }, + }, + ], + }); + }); + + equal(updates.length, 4, "all update items accepted"); + + equal(updates[0].targetApplications[0].maxVersion, "*"); + equal(updates[0].strictCompatibility, false); + + equal(updates[1].targetApplications[0].maxVersion, "43"); + equal(updates[1].strictCompatibility, true); + + equal(updates[2].targetApplications[0].maxVersion, "43"); + equal(updates[2].strictCompatibility, false); + + equal(updates[3].targetApplications[0].maxVersion, "44"); + equal(updates[3].strictCompatibility, true); + + messages = messages.filter(msg => + /Ignoring 'advisory_max_version'.*'strict_max_version' also present/.test( + msg.message + ) + ); + equal( + messages.length, + 1, + "mix of advisory_max_version and strict_max_version generated the expected warning" + ); +}); + +add_task(async function test_update_url_security() { + // Checks that update links to privileged URLs are not accepted. + + let { messages, result: updates } = await promiseConsoleOutput(() => { + return checkUpdates({ + id: "updatecheck-security@tests.mozilla.org", + version: "0.1", + updates: [ + { + version: "0.2", + update_link: "chrome://browser/content/browser.xhtml", + update_hash: + "sha256:08ac852190ecd81f40a514ea9299fe9143d9ab5e296b97e73fb2a314de49648a", + }, + { + version: "0.3", + update_link: "http://example.com/update.xpi", + update_hash: + "sha256:18ac852190ecd81f40a514ea9299fe9143d9ab5e296b97e73fb2a314de49648a", + }, + ], + }); + }); + + equal(updates.length, 2, "both updates were processed"); + equal(updates[0].updateURL, null, "privileged update URL was removed"); + equal( + updates[1].updateURL, + "http://example.com/update.xpi", + "safe update URL was accepted" + ); + + messages = messages.filter(msg => + /http:\/\/localhost.*\/updates\/.*may not load or link to chrome:/.test( + msg.message + ) + ); + equal( + messages.length, + 1, + "privileged update URL generated the expected console message" + ); +}); + +add_task(async function test_type_detection() { + // Checks that JSON update manifests are detected correctly + // regardless of extension or MIME type. + + let tests = [ + { contentType: "application/json", extension: "json", valid: true }, + { contentType: "application/json", extension: "php", valid: true }, + { contentType: "text/plain", extension: "json", valid: true }, + { contentType: "application/octet-stream", extension: "json", valid: true }, + { contentType: "text/plain", extension: "json?foo=bar", valid: true }, + { contentType: "text/plain", extension: "php", valid: true }, + { contentType: "text/plain", extension: "rdf", valid: true }, + { contentType: "application/json", extension: "rdf", valid: true }, + { contentType: "text/xml", extension: "json", valid: true }, + { contentType: "application/rdf+xml", extension: "json", valid: true }, + ]; + + for (let [i, test] of tests.entries()) { + let { messages } = await promiseConsoleOutput(async function () { + let id = `updatecheck-typedetection-${i}@tests.mozilla.org`; + let updates; + try { + updates = await checkUpdates({ + id, + version: "0.1", + contentType: test.contentType, + manifestExtension: test.extension, + updates: [{ version: "0.2" }], + }); + } catch (e) { + ok(!test.valid, "update manifest correctly detected as RDF"); + return; + } + + ok(test.valid, "update manifest correctly detected as JSON"); + equal(updates.length, 1, "correct number of updates"); + equal(updates[0].id, id, "update is for correct extension"); + }); + + if (test.valid) { + // Make sure we don't get any XML parsing errors from the + // XMLHttpRequest machinery. + ok( + !messages.some(msg => /not well-formed/.test(msg.message)), + "expect XMLHttpRequest not to attempt XML parsing" + ); + } + + messages = messages.filter(msg => + /Update manifest was not valid XML/.test(msg.message) + ); + equal( + messages.length, + !test.valid, + "expected number of XML parsing errors" + ); + } +}); + +add_task(async function test_empty_manifest() { + function checkUpdatesForUnlistedAddon(aData) { + // Registers an empty JSON update manifest with the test server to simulate + // the update server's actual response in the case of an unlisted add-on. + + let path = `/updates/${aData.id}.json`; + let updateUrl = `http://localhost:${gPort}${path}`; + + let manifestJSON = {}; + + mapManifest(path.replace(/\?.*/, ""), { + data: JSON.stringify(manifestJSON), + contentType: "application/json", + }); + + return new Promise((resolve, reject) => { + AddonUpdateChecker.checkForUpdates(aData.id, updateUrl, { + onUpdateCheckComplete: resolve, + + onUpdateCheckError(status) { + reject(new Error("Update check failed with status " + status)); + }, + }); + }); + } + + let { messages, result: updates } = await promiseConsoleOutput(() => { + return checkUpdatesForUnlistedAddon({ + id: "unlisted@example.org", + }); + }); + + equal(updates.length, 0, "no update could be found"); + + messages = messages.filter(msg => + /Received empty update manifest for .*/.test(msg.message) + ); + equal( + messages.length, + 1, + "unlisted addon generated the expected console message" + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_updateid.js b/toolkit/mozapps/extensions/test/xpcshell/test_updateid.js new file mode 100644 index 0000000000..c88c8e637b --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_updateid.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies that updating an add-on to a new ID does not work. + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false); + +let testserver = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); + +const ID = "updateid@tests.mozilla.org"; + +// Verify that an update to an add-on with a new ID fails +add_task(async function test_update_new_id() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + await promiseStartupManager(); + + await promiseInstallWebExtension({ + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: ID, + update_url: "http://example.com/update.json", + }, + }, + }, + }); + + let xpi = await createTempWebExtensionFile({ + manifest: { + version: "2.0", + browser_specific_settings: { + gecko: { id: "differentid@tests.mozilla.org" }, + }, + }, + }); + + testserver.registerFile("/addon.xpi", xpi); + AddonTestUtils.registerJSON(testserver, "/update.json", { + addons: { + [ID]: { + updates: [ + { + version: "2.0", + update_link: "http://example.com/addon.xpi", + applications: { + gecko: { + strict_min_version: "1", + strict_max_version: "10", + }, + }, + }, + ], + }, + }, + }); + + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.equal(addon.version, "1.0"); + + let update = await promiseFindAddonUpdates( + addon, + AddonManager.UPDATE_WHEN_USER_REQUESTED + ); + let install = update.updateAvailable; + Assert.notEqual(install, null, "Found available update"); + Assert.equal(install.name, addon.name); + Assert.equal(install.version, "2.0"); + Assert.equal(install.state, AddonManager.STATE_AVAILABLE); + Assert.equal(install.existingAddon, addon); + + await Assert.rejects( + install.install(), + err => install.error == AddonManager.ERROR_INCORRECT_ID, + "Upgrade to a different ID fails" + ); + + await addon.uninstall(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_updateversion.js b/toolkit/mozapps/extensions/test/xpcshell/test_updateversion.js new file mode 100644 index 0000000000..4d1510c40f --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_updateversion.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false); + +let server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); + +async function serverRegisterUpdate({ id, version, actualVersion }) { + let xpi = await createTempWebExtensionFile({ + manifest: { + version: actualVersion, + browser_specific_settings: { gecko: { id } }, + }, + }); + + server.registerFile("/addon.xpi", xpi); + AddonTestUtils.registerJSON(server, "/update.json", { + addons: { + [id]: { + updates: [{ version, update_link: "http://example.com/addon.xpi" }], + }, + }, + }); +} + +add_task(async function setup() { + await ExtensionTestUtils.startAddonManager(); +}); + +add_task(async function test_update_version_mismatch() { + const ID = "updateversion@tests.mozilla.org"; + await promiseInstallWebExtension({ + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: ID, + update_url: "http://example.com/update.json", + }, + }, + }, + }); + + await serverRegisterUpdate({ + id: ID, + version: "2.0", + actualVersion: "2.0.0", + }); + + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.equal(addon.version, "1.0"); + + let update = await promiseFindAddonUpdates( + addon, + AddonManager.UPDATE_WHEN_USER_REQUESTED + ); + let install = update.updateAvailable; + Assert.notEqual(install, false, "Found available update"); + Assert.equal(install.version, "2.0"); + Assert.equal(install.state, AddonManager.STATE_AVAILABLE); + Assert.equal(install.existingAddon, addon); + + await Assert.rejects( + install.install(), + err => install.error == AddonManager.ERROR_UNEXPECTED_ADDON_VERSION, + "Should refuse installation when downloaded version does not match" + ); + + await addon.uninstall(); +}); + +add_task(async function test_update_version_empty() { + const ID = "updateversionempty@tests.mozilla.org"; + await serverRegisterUpdate({ id: ID, version: "", actualVersion: "1.0" }); + + await promiseInstallWebExtension({ + manifest: { + version: "0", + browser_specific_settings: { + gecko: { + id: ID, + update_url: "http://example.com/update.json", + }, + }, + }, + }); + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + Assert.equal(addon.version, "0"); + let update = await promiseFindAddonUpdates( + addon, + AddonManager.UPDATE_WHEN_USER_REQUESTED + ); + // The only item in the updates array has version "" (empty). This should not + // be offered as an available update because it is certainly not newer. + Assert.equal(update.updateAvailable, false, "No update found"); + await addon.uninstall(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js b/toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js new file mode 100644 index 0000000000..6e5f221625 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js @@ -0,0 +1,199 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This verifies that app upgrades produce the expected behaviours, +// with strict compatibility checking disabled. + +Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false); + +// Enable loading extensions from the application scope +Services.prefs.setIntPref( + "extensions.enabledScopes", + AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION +); +Services.prefs.setIntPref("extensions.sideloadScopes", AddonManager.SCOPE_ALL); + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +const globalDir = Services.dirsvc.get("XREAddonAppDir", Ci.nsIFile); +globalDir.append("extensions"); + +var gGlobalExisted = globalDir.exists(); +var gInstallTime = Date.now(); + +const ID1 = "addon1@tests.mozilla.org"; +const ID2 = "addon2@tests.mozilla.org"; +const ID3 = "addon3@tests.mozilla.org"; +const ID4 = "addon4@tests.mozilla.org"; +const PATH4 = PathUtils.join(globalDir.path, `${ID4}.xpi`); + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + + // Will be compatible in the first version and incompatible in subsequent versions + let xpi = await createAddon({ + id: ID1, + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1", + }, + ], + targetPlatforms: [{ os: "XPCShell" }, { os: "WINNT_x86" }], + }); + await manuallyInstall(xpi, profileDir, ID1); + + // Works in all tested versions + xpi = await createAddon({ + id: ID2, + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "2", + }, + ], + targetPlatforms: [ + { + os: "XPCShell", + abi: "noarch-spidermonkey", + }, + ], + }); + await manuallyInstall(xpi, profileDir, ID2); + + // Will be disabled in the first version and enabled in the second. + xpi = createAddon({ + id: ID3, + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "2", + maxVersion: "2", + }, + ], + }); + await manuallyInstall(xpi, profileDir, ID3); + + // Will be compatible in both versions but will change version in between + xpi = await createAddon({ + id: ID4, + version: "1.0", + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1", + }, + ], + }); + await manuallyInstall(xpi, globalDir, ID4); + await promiseSetExtensionModifiedTime(PATH4, gInstallTime); +}); + +registerCleanupFunction(function end_test() { + if (!gGlobalExisted) { + globalDir.remove(true); + } else { + globalDir.append(do_get_expected_addon_name(ID4)); + globalDir.remove(true); + } +}); + +// Test that the test extensions are all installed +add_task(async function test_1() { + await promiseStartupManager(); + + let [a1, a2, a3, a4] = await promiseAddonsByIDs([ID1, ID2, ID3, ID4]); + Assert.notEqual(a1, null, "Found extension 1"); + Assert.equal(a1.isActive, true, "Extension 1 is active"); + + Assert.notEqual(a2, null, "Found extension 2"); + Assert.equal(a2.isActive, true, "Extension 2 is active"); + + Assert.notEqual(a3, null, "Found extension 3"); + Assert.equal(a3.isActive, false, "Extension 3 is not active"); + + Assert.notEqual(a4, null); + Assert.equal(a4.isActive, true); + Assert.equal(a4.version, "1.0"); +}); + +// Test that upgrading the application doesn't disable now incompatible add-ons +add_task(async function test_2() { + await promiseShutdownManager(); + + // Upgrade the extension + let xpi = createAddon({ + id: ID4, + version: "2.0", + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "2", + maxVersion: "2", + }, + ], + }); + await manuallyInstall(xpi, globalDir, ID4); + await promiseSetExtensionModifiedTime(PATH4, gInstallTime); + + await promiseStartupManager("2"); + let [a1, a2, a3, a4] = await promiseAddonsByIDs([ID1, ID2, ID3, ID4]); + Assert.notEqual(a1, null); + Assert.ok(isExtensionInBootstrappedList(profileDir, a1.id)); + + Assert.notEqual(a2, null); + Assert.ok(isExtensionInBootstrappedList(profileDir, a2.id)); + + Assert.notEqual(a3, null); + Assert.ok(isExtensionInBootstrappedList(profileDir, a3.id)); + + Assert.notEqual(a4, null); + Assert.ok(isExtensionInBootstrappedList(globalDir, a4.id)); + Assert.equal(a4.version, "2.0"); +}); + +// Test that nothing changes when only the build ID changes. +add_task(async function test_3() { + await promiseShutdownManager(); + + // Upgrade the extension + let xpi = createAddon({ + id: ID4, + version: "3.0", + targetApplications: [ + { + id: "xpcshell@tests.mozilla.org", + minVersion: "3", + maxVersion: "3", + }, + ], + }); + await manuallyInstall(xpi, globalDir, ID4); + await promiseSetExtensionModifiedTime(PATH4, gInstallTime); + + // Simulates a simple Build ID change + gAddonStartup.remove(true); + await promiseStartupManager(); + + let [a1, a2, a3, a4] = await promiseAddonsByIDs([ID1, ID2, ID3, ID4]); + + Assert.notEqual(a1, null); + Assert.ok(isExtensionInBootstrappedList(profileDir, a1.id)); + + Assert.notEqual(a2, null); + Assert.ok(isExtensionInBootstrappedList(profileDir, a2.id)); + + Assert.notEqual(a3, null); + Assert.ok(isExtensionInBootstrappedList(profileDir, a3.id)); + + Assert.notEqual(a4, null); + Assert.ok(isExtensionInBootstrappedList(globalDir, a4.id)); + Assert.equal(a4.version, "2.0"); + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_upgrade_incompatible.js b/toolkit/mozapps/extensions/test/xpcshell/test_upgrade_incompatible.js new file mode 100644 index 0000000000..b9eb0f31e1 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_upgrade_incompatible.js @@ -0,0 +1,73 @@ +// Tests that when an extension manifest that was previously valid becomes +// unparseable after an application update, the extension becomes +// disabled. (See bug 1439600 for a concrete example of a situation where +// this happened). +add_task(async function test_upgrade_incompatible() { + const ID = "incompatible-upgrade@tests.mozilla.org"; + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + + await promiseStartupManager(); + + let file = createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { gecko: { id: ID } }, + }, + }); + + let { addon } = await promiseInstallFile(file); + + notEqual(addon, null); + equal(addon.appDisabled, false); + + await promiseShutdownManager(); + + // Create a new, incompatible extension + let newfile = createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { gecko: { id: ID } }, + manifest_version: 1, + }, + }); + + // swap the incompatible extension in for the original + let path = PathUtils.join(gProfD.path, "extensions", `${ID}.xpi`); + let fileInfo = await IOUtils.stat(path); + let timestamp = fileInfo.lastModified; + + await IOUtils.move(newfile.path, path); + await promiseSetExtensionModifiedTime(path, timestamp); + Services.obs.notifyObservers(new FileUtils.File(path), "flush-cache-entry"); + + // Restart. With the change to the DB schema we recompute compatibility. + // With an unparseable manifest the addon should become disabled. + Services.prefs.setIntPref("extensions.databaseSchema", 0); + await promiseStartupManager(); + + addon = await promiseAddonByID(ID); + notEqual(addon, null); + equal(addon.appDisabled, true); + + await promiseShutdownManager(); + + file = createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { gecko: { id: ID } }, + }, + }); + + // swap the old extension back in and check that we don't persist the disabled state forever. + await IOUtils.move(file.path, path); + await promiseSetExtensionModifiedTime(path, timestamp); + Services.obs.notifyObservers(new FileUtils.File(path), "flush-cache-entry"); + + // Restart. With the change to the DB schema we recompute compatibility. + Services.prefs.setIntPref("extensions.databaseSchema", 0); + await promiseStartupManager(); + + addon = await promiseAddonByID(ID); + notEqual(addon, null); + equal(addon.appDisabled, false); + + await promiseShutdownManager(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js new file mode 100644 index 0000000000..cd4b376117 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js @@ -0,0 +1,676 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const ID = "webextension1@tests.mozilla.org"; + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +const ADDONS = { + webextension_1: { + "manifest.json": { + name: "Web Extension Name", + version: "1.0", + manifest_version: 2, + browser_specific_settings: { + gecko: { + id: "webextension1@tests.mozilla.org", + }, + }, + icons: { + 48: "icon48.png", + 64: "icon64.png", + }, + }, + "chrome.manifest": "content webex ./\n", + }, + webextension_3: { + "manifest.json": { + name: "Web Extensiøn __MSG_name__", + description: "Descriptïon __MSG_desc__ of add-on", + version: "1.0", + manifest_version: 2, + default_locale: "en", + browser_specific_settings: { + gecko: { + id: "webextension3@tests.mozilla.org", + }, + }, + }, + "_locales/en/messages.json": { + name: { + message: "foo ☹", + description: "foo", + }, + desc: { + message: "bar ☹", + description: "bar", + }, + }, + "_locales/fr/messages.json": { + name: { + message: "le foo ☺", + description: "foo", + }, + desc: { + message: "le bar ☺", + description: "bar", + }, + }, + }, +}; + +let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService( + Ci.nsIChromeRegistry +); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +const { + ExtensionParent: { GlobalManager }, +} = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionParent.sys.mjs" +); + +add_task(async function test_1() { + await promiseStartupManager(); + + equal(GlobalManager.extensionMap.size, 0); + + let { addon } = await AddonTestUtils.promiseInstallXPI(ADDONS.webextension_1); + + equal(GlobalManager.extensionMap.size, 1); + ok(GlobalManager.extensionMap.has(ID)); + + Assert.throws( + () => + chromeReg.convertChromeURL( + Services.io.newURI("chrome://webex/content/webex.xul") + ), + error => error.result == Cr.NS_ERROR_FILE_NOT_FOUND, + "Chrome manifest should not have been registered" + ); + + let uri = do_get_addon_root_uri(profileDir, ID); + + checkAddon(ID, addon, { + version: "1.0", + name: "Web Extension Name", + isCompatible: true, + appDisabled: false, + isActive: true, + isSystem: false, + type: "extension", + isWebExtension: true, + signedState: AddonManager.SIGNEDSTATE_PRIVILEGED, + iconURL: `${uri}icon48.png`, + }); + + // Should persist through a restart + await promiseShutdownManager(); + + equal(GlobalManager.extensionMap.size, 0); + + await promiseStartupManager(); + + equal(GlobalManager.extensionMap.size, 1); + ok(GlobalManager.extensionMap.has(ID)); + + addon = await promiseAddonByID(ID); + + uri = do_get_addon_root_uri(profileDir, ID); + + checkAddon(ID, addon, { + version: "1.0", + name: "Web Extension Name", + isCompatible: true, + appDisabled: false, + isActive: true, + isSystem: false, + type: "extension", + signedState: AddonManager.SIGNEDSTATE_PRIVILEGED, + iconURL: `${uri}icon48.png`, + }); + + await addon.disable(); + + equal(GlobalManager.extensionMap.size, 0); + + await addon.enable(); + + equal(GlobalManager.extensionMap.size, 1); + ok(GlobalManager.extensionMap.has(ID)); + + await addon.uninstall(); + + equal(GlobalManager.extensionMap.size, 0); + Assert.ok(!GlobalManager.extensionMap.has(ID)); + + await promiseShutdownManager(); +}); + +// Writing the manifest direct to the profile should work +add_task(async function test_2() { + await promiseWriteWebManifestForExtension( + { + name: "Web Extension Name", + version: "1.0", + manifest_version: 2, + browser_specific_settings: { + gecko: { + id: ID, + }, + }, + }, + profileDir + ); + + await promiseStartupManager(); + + let addon = await promiseAddonByID(ID); + checkAddon(ID, addon, { + version: "1.0", + name: "Web Extension Name", + isCompatible: true, + appDisabled: false, + isActive: true, + isSystem: false, + type: "extension", + signedState: AddonManager.SIGNEDSTATE_PRIVILEGED, + }); + + await addon.uninstall(); + + await promiseRestartManager(); +}); + +add_task(async function test_manifest_localization() { + const extensionId = "webextension3@tests.mozilla.org"; + + let { addon } = await AddonTestUtils.promiseInstallXPI(ADDONS.webextension_3); + + await addon.disable(); + + checkAddon(ID, addon, { + name: "Web Extensiøn foo ☹", + description: "Descriptïon bar ☹ of add-on", + }); + + await restartWithLocales(["fr-FR"]); + + addon = await promiseAddonByID(extensionId); + checkAddon(ID, addon, { + name: "Web Extensiøn le foo ☺", + description: "Descriptïon le bar ☺ of add-on", + }); + + await restartWithLocales(["de"]); + + addon = await promiseAddonByID(extensionId); + checkAddon(ID, addon, { + name: "Web Extensiøn foo ☹", + description: "Descriptïon bar ☹ of add-on", + }); + + await addon.uninstall(); +}); + +// Missing version should cause a failure +add_task(async function test_3() { + await promiseWriteWebManifestForExtension( + { + name: "Web Extension Name", + manifest_version: 2, + browser_specific_settings: { + gecko: { + id: ID, + }, + }, + }, + profileDir + ); + + await promiseRestartManager(); + + let addon = await promiseAddonByID(ID); + Assert.equal(addon, null); + + let file = getFileForAddon(profileDir, ID); + Assert.ok(!file.exists()); + + await promiseRestartManager(); +}); + +// Incorrect manifest version should cause a failure +add_task(async function test_4() { + await promiseWriteWebManifestForExtension( + { + name: "Web Extension Name", + version: "1.0", + manifest_version: 1, + browser_specific_settings: { + gecko: { + id: ID, + }, + }, + }, + profileDir + ); + + await promiseRestartManager(); + + let addon = await promiseAddonByID(ID); + Assert.equal(addon, null); + + let file = getFileForAddon(profileDir, ID); + Assert.ok(!file.exists()); + + await promiseRestartManager(); +}); + +// Test that the "options_ui" manifest section is processed correctly. +add_task(async function test_options_ui() { + let OPTIONS_RE = + /^moz-extension:\/\/[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}\/options\.html$/; + + const extensionId = "webextension@tests.mozilla.org"; + let addon = await promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { gecko: { id: extensionId } }, + options_ui: { + page: "options.html", + }, + }, + }); + + checkAddon(extensionId, addon, { + optionsType: AddonManager.OPTIONS_TYPE_INLINE_BROWSER, + }); + + ok( + OPTIONS_RE.test(addon.optionsURL), + "Addon should have a moz-extension: options URL for /options.html" + ); + + await addon.uninstall(); + + const ID2 = "webextension2@tests.mozilla.org"; + addon = await promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { gecko: { id: ID2 } }, + options_ui: { + page: "options.html", + open_in_tab: true, + }, + }, + }); + + checkAddon(ID2, addon, { + optionsType: AddonManager.OPTIONS_TYPE_TAB, + }); + + ok( + OPTIONS_RE.test(addon.optionsURL), + "Addon should have a moz-extension: options URL for /options.html" + ); + + await addon.uninstall(); +}); + +// Test that experiments permissions add the appropriate dependencies. +add_task(async function test_experiments_dependencies() { + let addon = await promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { gecko: { id: "meh@experiment" } }, + permissions: ["experiments.meh"], + }, + }); + + checkAddon(addon.id, addon, { + dependencies: ["meh@experiments.addons.mozilla.org"], + // Add-on should be app disabled due to missing dependencies + appDisabled: true, + }); + + await addon.uninstall(); +}); + +add_task(async function developerShouldOverride() { + let addon = await promiseInstallWebExtension({ + manifest: { + default_locale: "en", + developer: { + name: "__MSG_name__", + url: "__MSG_url__", + }, + author: "Will be overridden by developer", + homepage_url: "https://will.be.overridden", + }, + files: { + "_locales/en/messages.json": `{ + "name": { + "message": "en name" + }, + "url": { + "message": "https://example.net/en" + } + }`, + }, + }); + + checkAddon(ID, addon, { + creator: "en name", + homepageURL: "https://example.net/en", + }); + + await addon.uninstall(); +}); + +add_task(async function test_invalid_developer_does_not_override() { + for (const { type, manifestProps, files } of [ + { + type: "dictionary", + manifestProps: { + dictionaries: { + "en-US": "en-US.dic", + }, + }, + files: { + "en-US.dic": "", + "en-US.aff": "", + }, + }, + { + type: "theme", + manifestProps: { + theme: { + colors: { + frame: "#FFF", + tab_background_text: "#000", + }, + }, + }, + }, + { + type: "locale", + manifestProps: { + langpack_id: "und", + languages: { + und: { + chrome_resources: { + global: "chrome/und/locale/und/global", + }, + version: "20190326174300", + }, + }, + }, + }, + ]) { + const id = `${type}@mozilla.com`; + const creator = "Some author"; + const homepageURL = "https://example.net"; + + info(`== loading add-on with id=${id} ==`); + + for (let developer of [{}, null, { name: null, url: null }]) { + let addon = await promiseInstallWebExtension({ + manifest: { + author: creator, + homepage_url: homepageURL, + developer, + browser_specific_settings: { gecko: { id } }, + ...manifestProps, + }, + files, + }); + + checkAddon(id, addon, { type, creator, homepageURL }); + + await addon.uninstall(); + } + } +}); + +add_task(async function authorNotString() { + ExtensionTestUtils.failOnSchemaWarnings(false); + for (let author of [{}, [], 42]) { + let addon = await promiseInstallWebExtension({ + manifest: { + author, + manifest_version: 2, + name: "Web Extension Name", + version: "1.0", + }, + }); + + checkAddon(ID, addon, { + creator: null, + }); + + await addon.uninstall(); + } + ExtensionTestUtils.failOnSchemaWarnings(true); +}); + +add_task(async function testThemeExtension() { + let addon = await promiseInstallWebExtension({ + manifest: { + author: "Some author", + manifest_version: 2, + name: "Web Extension Name", + version: "1.0", + theme: { images: { theme_frame: "example.png" } }, + }, + }); + + checkAddon(ID, addon, { + creator: "Some author", + version: "1.0", + name: "Web Extension Name", + isCompatible: true, + appDisabled: false, + isActive: false, + userDisabled: true, + isSystem: false, + type: "theme", + isWebExtension: true, + signedState: AddonManager.SIGNEDSTATE_PRIVILEGED, + }); + + await addon.uninstall(); + + // Also test one without a proper 'theme' section. + ExtensionTestUtils.failOnSchemaWarnings(false); + addon = await promiseInstallWebExtension({ + manifest: { + author: "Some author", + manifest_version: 2, + name: "Web Extension Name", + version: "1.0", + theme: null, + }, + }); + ExtensionTestUtils.failOnSchemaWarnings(true); + + checkAddon(ID, addon, { + type: "extension", + isWebExtension: true, + }); + + await addon.uninstall(); +}); + +// Test that we can update from a webextension to a webextension-theme +add_task(async function test_theme_upgrade() { + // First install a regular webextension + let addon = await promiseInstallWebExtension({ + manifest: { + version: "1.0", + name: "Test WebExtension 1 (temporary)", + browser_specific_settings: { + gecko: { + id: ID, + }, + }, + }, + }); + + checkAddon(ID, addon, { + version: "1.0", + name: "Test WebExtension 1 (temporary)", + isCompatible: true, + appDisabled: false, + isActive: true, + type: "extension", + signedState: AddonManager.SIGNEDSTATE_PRIVILEGED, + }); + + // Create a webextension theme with the same ID + addon = await promiseInstallWebExtension({ + manifest: { + version: "2.0", + name: "Test WebExtension 1 (temporary)", + browser_specific_settings: { + gecko: { + id: ID, + }, + }, + theme: { images: { theme_frame: "example.png" } }, + }, + }); + + checkAddon(ID, addon, { + version: "2.0", + name: "Test WebExtension 1 (temporary)", + isCompatible: true, + appDisabled: false, + isActive: true, + // This is what we're really interested in: + type: "theme", + isWebExtension: true, + }); + + await addon.uninstall(); + + addon = await promiseAddonByID(ID); + Assert.equal(addon, null); +}); + +add_task(async function test_developer_properties() { + const name = "developer-name"; + const url = "https://example.org"; + + for (const { type, manifestProps, files } of [ + { + type: "dictionary", + manifestProps: { + dictionaries: { + "en-US": "en-US.dic", + }, + }, + files: { + "en-US.dic": "", + "en-US.aff": "", + }, + }, + { + type: "statictheme", + manifestProps: { + theme: { + colors: { + frame: "#FFF", + tab_background_text: "#000", + }, + }, + }, + }, + { + type: "langpack", + manifestProps: { + langpack_id: "und", + languages: { + und: { + chrome_resources: { + global: "chrome/und/locale/und/global", + }, + version: "20190326174300", + }, + }, + }, + }, + ]) { + const id = `${type}@mozilla.com`; + + info(`== loading add-on with id=${id} ==`); + + let addon = await promiseInstallWebExtension({ + manifest: { + developer: { + name, + url, + }, + author: "Will be overridden by developer", + homepage_url: "https://will.be.overridden", + browser_specific_settings: { gecko: { id } }, + ...manifestProps, + }, + files, + }); + + checkAddon(id, addon, { creator: name, homepageURL: url }); + + await addon.uninstall(); + } +}); + +add_task(async function test_invalid_homepage_and_developer_urls() { + const INVALID_URLS = [ + "chrome://browser/content/", + "data:text/json,...", + "javascript:;", + "/", + "not-an-url", + ]; + const EXPECTED_ERROR_RE = + /Access denied for URL|may not load or link to|is not a valid URL/; + + for (let url of INVALID_URLS) { + // First, we verify `homepage_url`, which has a `url` "format" defined + // since it exists. + let normalized = await ExtensionTestUtils.normalizeManifest({ + homepage_url: url, + }); + ok( + EXPECTED_ERROR_RE.test(normalized.error), + `got expected error for ${url}` + ); + + // The `developer.url` now has a "format" but it was a late addition so we + // are only raising a warning instead of an error. + ExtensionTestUtils.failOnSchemaWarnings(false); + normalized = await ExtensionTestUtils.normalizeManifest({ + developer: { url }, + }); + ok(!normalized.error, "expected no error"); + ok( + // Despites this prop being named `errors`, we are checking the warnings + // here. + EXPECTED_ERROR_RE.test(normalized.errors[0]), + `got expected warning for ${url}` + ); + ExtensionTestUtils.failOnSchemaWarnings(true); + } +}); + +add_task(async function test_valid_homepage_and_developer_urls() { + let normalized = await ExtensionTestUtils.normalizeManifest({ + developer: { url: "https://example.com" }, + }); + ok(!normalized.error, "expected no error"); + + normalized = await ExtensionTestUtils.normalizeManifest({ + homepage_url: "https://example.com", + }); + ok(!normalized.error, "expected no error"); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_events.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_events.js new file mode 100644 index 0000000000..dd4222ec88 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_events.js @@ -0,0 +1,94 @@ +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +add_task(async function () { + let triggered = {}; + const { Management } = ChromeUtils.importESModule( + "resource://gre/modules/Extension.sys.mjs" + ); + for (let event of ["install", "uninstall", "update"]) { + triggered[event] = false; + Management.on(event, () => (triggered[event] = true)); + } + + async function expectEvents(expected, fn) { + let events = Object.keys(expected); + for (let event of events) { + triggered[event] = false; + } + + await fn(); + await new Promise(executeSoon); + + for (let event of events) { + equal( + triggered[event], + expected[event], + `Event ${event} was${expected[event] ? "" : " not"} triggered` + ); + } + } + + await promiseStartupManager(); + + const id = "webextension@tests.mozilla.org"; + + // Install version 1.0, shouldn't see any events + await expectEvents({ update: false, uninstall: false }, async () => { + await promiseInstallWebExtension({ + manifest: { + version: "1.0", + browser_specific_settings: { gecko: { id } }, + }, + }); + }); + + // Install version 2.0, we should get an update event but not an uninstall + await expectEvents({ update: true, uninstall: false }, async () => { + await promiseInstallWebExtension({ + manifest: { + version: "2.0", + browser_specific_settings: { gecko: { id } }, + }, + }); + }); + + // Install version 3.0 as a temporary addon, we should again get + // update but not uninstall + let v3 = createTempWebExtensionFile({ + manifest: { + version: "3.0", + browser_specific_settings: { gecko: { id } }, + }, + }); + + await expectEvents({ update: true, uninstall: false }, () => + AddonManager.installTemporaryAddon(v3) + ); + + // Uninstall the temporary addon, this causes version 2.0 still installed + // in the profile to be revealed. Again, this results in an update event. + let addon = await promiseAddonByID(id); + await expectEvents({ update: true, uninstall: false }, () => + addon.uninstall() + ); + + // Re-install version 3.0 as a temporary addon + await AddonManager.installTemporaryAddon(v3); + + // Now shut down the addons manager, this should cause the temporary + // addon to be uninstalled which reveals 2.0 from the profile. + await expectEvents({ update: true, uninstall: false }, () => + promiseShutdownManager() + ); + + // When we start up again we should not see any events + await expectEvents({ install: false }, () => promiseStartupManager()); + + addon = await promiseAddonByID(id); + + // When we uninstall from the profile, the addon is now gone, we should + // get an uninstall events. + await expectEvents({ update: false, uninstall: true }, () => + addon.uninstall() + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js new file mode 100644 index 0000000000..112a2c8410 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js @@ -0,0 +1,212 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const ID = "webextension1@tests.mozilla.org"; + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); +profileDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +async function testSimpleIconsetParsing(manifest) { + await promiseWriteWebManifestForExtension(manifest, profileDir); + + await promiseRestartManager(); + + let uri = do_get_addon_root_uri(profileDir, ID); + + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + + function check_icons(addon_copy) { + deepEqual(addon_copy.icons, { + 16: uri + "icon16.png", + 32: uri + "icon32.png", + 48: uri + "icon48.png", + 64: uri + "icon64.png", + }); + + // iconURL should map to icons[48] + equal(addon.iconURL, uri + "icon48.png"); + + // AddonManager gets the correct icon sizes from addon.icons + equal(AddonManager.getPreferredIconURL(addon, 1), uri + "icon16.png"); + equal(AddonManager.getPreferredIconURL(addon, 16), uri + "icon16.png"); + equal(AddonManager.getPreferredIconURL(addon, 30), uri + "icon32.png"); + equal(AddonManager.getPreferredIconURL(addon, 48), uri + "icon48.png"); + equal(AddonManager.getPreferredIconURL(addon, 64), uri + "icon64.png"); + equal(AddonManager.getPreferredIconURL(addon, 128), uri + "icon64.png"); + } + + check_icons(addon); + + // check if icons are persisted through a restart + await promiseRestartManager(); + + addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + + check_icons(addon); + + await addon.uninstall(); +} + +async function testRetinaIconsetParsing(manifest) { + await promiseWriteWebManifestForExtension(manifest, profileDir); + + await promiseRestartManager(); + + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + + let uri = do_get_addon_root_uri(profileDir, ID); + + // AddonManager displays larger icons for higher pixel density + equal( + AddonManager.getPreferredIconURL(addon, 32, { + devicePixelRatio: 2, + }), + uri + "icon64.png" + ); + + equal( + AddonManager.getPreferredIconURL(addon, 48, { + devicePixelRatio: 2, + }), + uri + "icon128.png" + ); + + equal( + AddonManager.getPreferredIconURL(addon, 64, { + devicePixelRatio: 2, + }), + uri + "icon128.png" + ); + + await addon.uninstall(); +} + +async function testNoIconsParsing(manifest) { + await promiseWriteWebManifestForExtension(manifest, profileDir); + + await promiseRestartManager(); + + let addon = await promiseAddonByID(ID); + Assert.notEqual(addon, null); + + deepEqual(addon.icons, {}); + + equal(addon.iconURL, null); + + equal(AddonManager.getPreferredIconURL(addon, 128), null); + + await addon.uninstall(); +} + +// Test simple icon set parsing +add_task(async function () { + await promiseStartupManager(); + await testSimpleIconsetParsing({ + name: "Web Extension Name", + version: "1.0", + manifest_version: 2, + browser_specific_settings: { + gecko: { + id: ID, + }, + }, + icons: { + 16: "icon16.png", + 32: "icon32.png", + 48: "icon48.png", + 64: "icon64.png", + }, + }); + + // Now for theme-type extensions too. + await testSimpleIconsetParsing({ + name: "Web Extension Name", + version: "1.0", + manifest_version: 2, + browser_specific_settings: { + gecko: { + id: ID, + }, + }, + icons: { + 16: "icon16.png", + 32: "icon32.png", + 48: "icon48.png", + 64: "icon64.png", + }, + theme: { images: { theme_frame: "example.png" } }, + }); +}); + +// Test AddonManager.getPreferredIconURL for retina screen sizes +add_task(async function () { + await testRetinaIconsetParsing({ + name: "Web Extension Name", + version: "1.0", + manifest_version: 2, + browser_specific_settings: { + gecko: { + id: ID, + }, + }, + icons: { + 32: "icon32.png", + 48: "icon48.png", + 64: "icon64.png", + 128: "icon128.png", + 256: "icon256.png", + }, + }); + + await testRetinaIconsetParsing({ + name: "Web Extension Name", + version: "1.0", + manifest_version: 2, + browser_specific_settings: { + gecko: { + id: ID, + }, + }, + icons: { + 32: "icon32.png", + 48: "icon48.png", + 64: "icon64.png", + 128: "icon128.png", + 256: "icon256.png", + }, + theme: { images: { theme_frame: "example.png" } }, + }); +}); + +// Handles no icons gracefully +add_task(async function () { + await testNoIconsParsing({ + name: "Web Extension Name", + version: "1.0", + manifest_version: 2, + browser_specific_settings: { + gecko: { + id: ID, + }, + }, + }); + + await testNoIconsParsing({ + name: "Web Extension Name", + version: "1.0", + manifest_version: 2, + browser_specific_settings: { + gecko: { + id: ID, + }, + }, + theme: { images: { theme_frame: "example.png" } }, + }); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js new file mode 100644 index 0000000000..1b2080e8db --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js @@ -0,0 +1,696 @@ +let profileDir; +add_task(async function setup() { + profileDir = gProfD.clone(); + profileDir.append("extensions"); + + if (!profileDir.exists()) { + profileDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + } + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + await promiseStartupManager(); +}); + +const IMPLICIT_ID_XPI = "data/webext-implicit-id.xpi"; +const IMPLICIT_ID_ID = "webext_implicit_id@tests.mozilla.org"; + +// webext-implicit-id.xpi has a minimal manifest with no +// applications or browser_specific_settings, so its id comes +// from its signature, which should be the ID constant defined below. +add_task(async function test_implicit_id() { + let addon = await promiseAddonByID(IMPLICIT_ID_ID); + equal(addon, null, "Add-on is not installed"); + + await promiseInstallFile(do_get_file(IMPLICIT_ID_XPI)); + + addon = await promiseAddonByID(IMPLICIT_ID_ID); + notEqual(addon, null, "Add-on is installed"); + + await addon.uninstall(); +}); + +// We should also be able to install webext-implicit-id.xpi temporarily +// and it should look just like the regular install (ie, the ID should +// come from the signature) +add_task(async function test_implicit_id_temp() { + let addon = await promiseAddonByID(IMPLICIT_ID_ID); + equal(addon, null, "Add-on is not installed"); + + let xpifile = do_get_file(IMPLICIT_ID_XPI); + await AddonManager.installTemporaryAddon(xpifile); + + addon = await promiseAddonByID(IMPLICIT_ID_ID); + notEqual(addon, null, "Add-on is installed"); + + // The sourceURI of a temporary installed addon should be equal to the + // file url of the installed xpi file. + equal( + addon.sourceURI && addon.sourceURI.spec, + Services.io.newFileURI(xpifile).spec, + "SourceURI of the add-on has the expected value" + ); + + await addon.uninstall(); +}); + +// Test that extension install error attach the detailed error messages to the +// Error object. +add_task(async function test_invalid_extension_install_errors() { + const manifest = { + name: "invalid", + browser_specific_settings: { + gecko: { + id: "invalid@tests.mozilla.org", + }, + }, + description: "extension with an invalid 'matches' value", + manifest_version: 2, + content_scripts: [ + { + matches: "*://*.foo.com/*", + js: ["content.js"], + }, + ], + version: "1.0", + }; + + const addonDir = await promiseWriteWebManifestForExtension( + manifest, + gTmpD, + "the-addon-sub-dir" + ); + + await Assert.rejects( + AddonManager.installTemporaryAddon(addonDir), + err => { + return ( + err.additionalErrors.length == 1 && + err.additionalErrors[0] == + `Reading manifest: Error processing content_scripts.0.matches: ` + + `Expected array instead of "*://*.foo.com/*"` + ); + }, + "Exception has the proper additionalErrors details" + ); + + Services.obs.notifyObservers(addonDir, "flush-cache-entry"); + addonDir.remove(true); +}); + +// We should be able to temporarily install an unsigned web extension +// that does not have an ID in its manifest. +add_task(async function test_unsigned_no_id_temp_install() { + AddonTestUtils.useRealCertChecks = true; + const manifest = { + name: "no ID", + description: "extension without an ID", + manifest_version: 2, + version: "1.0", + }; + + const addonDir = await promiseWriteWebManifestForExtension( + manifest, + gTmpD, + "the-addon-sub-dir" + ); + const testDate = new Date(); + const addon = await AddonManager.installTemporaryAddon(addonDir); + + ok(addon.id, "ID should have been auto-generated"); + Assert.less( + Math.abs(addon.installDate - testDate), + 10000, + "addon has an expected installDate" + ); + Assert.less( + Math.abs(addon.updateDate - testDate), + 10000, + "addon has an expected updateDate" + ); + + // The sourceURI of a temporary installed addon should be equal to the + // file url of the installed source dir. + equal( + addon.sourceURI && addon.sourceURI.spec, + Services.io.newFileURI(addonDir).spec, + "SourceURI of the add-on has the expected value" + ); + + // Install the same directory again, as if re-installing or reloading. + const secondAddon = await AddonManager.installTemporaryAddon(addonDir); + // The IDs should be the same. + equal(secondAddon.id, addon.id, "Reinstalled add-on has the expected ID"); + equal( + secondAddon.installDate.valueOf(), + addon.installDate.valueOf(), + "Reloaded add-on has the expected installDate." + ); + + await secondAddon.uninstall(); + Services.obs.notifyObservers(addonDir, "flush-cache-entry"); + addonDir.remove(true); + AddonTestUtils.useRealCertChecks = false; +}); + +// We should be able to install two extensions from manifests without IDs +// at different locations and get two unique extensions. +add_task(async function test_multiple_no_id_extensions() { + AddonTestUtils.useRealCertChecks = true; + const manifest = { + name: "no ID", + description: "extension without an ID", + manifest_version: 2, + version: "1.0", + }; + + let extension1 = ExtensionTestUtils.loadExtension({ + manifest, + useAddonManager: "temporary", + }); + + let extension2 = ExtensionTestUtils.loadExtension({ + manifest, + useAddonManager: "temporary", + }); + + await Promise.all([extension1.startup(), extension2.startup()]); + + const allAddons = await AddonManager.getAllAddons(); + + info(`Found these add-ons: ${allAddons.map(a => a.name).join(", ")}`); + const filtered = allAddons.filter(addon => addon.name === manifest.name); + // Make sure we have two add-ons by the same name. + equal(filtered.length, 2, "Two add-ons are installed with the same name"); + + await extension1.unload(); + await extension2.unload(); + AddonTestUtils.useRealCertChecks = false; +}); + +// Test that we can get the ID from browser_specific_settings +add_task(async function test_bss_id() { + const ID = "webext_bss_id@tests.mozilla.org"; + + let manifest = { + name: "bss test", + description: "test that ID may be in browser_specific_settings", + manifest_version: 2, + version: "1.0", + + browser_specific_settings: { + gecko: { + id: ID, + }, + }, + }; + + let addon = await promiseAddonByID(ID); + equal(addon, null, "Add-on is not installed"); + + let extension = ExtensionTestUtils.loadExtension({ + manifest, + useAddonManager: "temporary", + }); + await extension.startup(); + + addon = await promiseAddonByID(ID); + notEqual(addon, null, "Add-on is installed"); + + await extension.unload(); +}); + +// Test that if we have IDs in both browser_specific_settings and applications, +// that we prefer the ID in browser_specific_settings. +add_task(async function test_two_ids() { + const GOOD_ID = "two_ids@tests.mozilla.org"; + const BAD_ID = "i_am_obsolete@tests.mozilla.org"; + + let manifest = { + name: "two id test", + description: + "test a web extension with ids in both applications and browser_specific_settings", + manifest_version: 2, + version: "1.0", + + applications: { + gecko: { + id: BAD_ID, + }, + }, + + browser_specific_settings: { + gecko: { + id: GOOD_ID, + }, + }, + }; + + let extension = ExtensionTestUtils.loadExtension({ + manifest, + useAddonManager: "temporary", + }); + await extension.startup(); + + let addon = await promiseAddonByID(BAD_ID); + equal(addon, null, "Add-on is not found using bad ID"); + addon = await promiseAddonByID(GOOD_ID); + notEqual(addon, null, "Add-on is found using good ID"); + + await extension.unload(); +}); + +// Test that strict_min_version and strict_max_version are enforced for +// loading temporary extension. +add_task(async function test_strict_min_max() { + // the app version being compared to is 1.9.2 + const addonId = "strict_min_max@tests.mozilla.org"; + const MANIFEST = { + name: "strict min max test", + description: "test strict min and max with temporary loading", + manifest_version: 2, + version: "1.0", + }; + + // bad max good min + let apps = { + browser_specific_settings: { + gecko: { + id: addonId, + strict_min_version: "1", + strict_max_version: "1", + }, + }, + }; + let testManifest = Object.assign(apps, MANIFEST); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: testManifest, + useAddonManager: "temporary", + }); + + let expectedMsg = new RegExp( + "Add-on strict_min_max@tests.mozilla.org is not compatible with application version. " + + "add-on minVersion: 1. add-on maxVersion: 1." + ); + await Assert.rejects( + extension.startup(), + expectedMsg, + "Install rejects when specified maxVersion is not valid" + ); + + let addon = await promiseAddonByID(addonId); + equal(addon, null, "Add-on is not installed"); + + // bad min good max + apps = { + browser_specific_settings: { + gecko: { + id: addonId, + strict_min_version: "2", + strict_max_version: "2", + }, + }, + }; + testManifest = Object.assign(apps, MANIFEST); + + extension = ExtensionTestUtils.loadExtension({ + manifest: testManifest, + useAddonManager: "temporary", + }); + + expectedMsg = new RegExp( + "Add-on strict_min_max@tests.mozilla.org is not compatible with application version. " + + "add-on minVersion: 2. add-on maxVersion: 2." + ); + await Assert.rejects( + extension.startup(), + expectedMsg, + "Install rejects when specified minVersion is not valid" + ); + + addon = await promiseAddonByID(addonId); + equal(addon, null, "Add-on is not installed"); + + // bad both + apps = { + browser_specific_settings: { + gecko: { + id: addonId, + strict_min_version: "2", + strict_max_version: "1", + }, + }, + }; + testManifest = Object.assign(apps, MANIFEST); + + extension = ExtensionTestUtils.loadExtension({ + manifest: testManifest, + useAddonManager: "temporary", + }); + + expectedMsg = new RegExp( + "Add-on strict_min_max@tests.mozilla.org is not compatible with application version. " + + "add-on minVersion: 2. add-on maxVersion: 1." + ); + await Assert.rejects( + extension.startup(), + expectedMsg, + "Install rejects when specified minVersion and maxVersion are not valid" + ); + + addon = await promiseAddonByID(addonId); + equal(addon, null, "Add-on is not installed"); + + // bad only min + apps = { + browser_specific_settings: { + gecko: { + id: addonId, + strict_min_version: "2", + }, + }, + }; + testManifest = Object.assign(apps, MANIFEST); + + extension = ExtensionTestUtils.loadExtension({ + manifest: testManifest, + useAddonManager: "temporary", + }); + + expectedMsg = new RegExp( + "Add-on strict_min_max@tests.mozilla.org is not compatible with application version. " + + "add-on minVersion: 2." + ); + await Assert.rejects( + extension.startup(), + expectedMsg, + "Install rejects when specified minVersion and maxVersion are not valid" + ); + + addon = await promiseAddonByID(addonId); + equal(addon, null, "Add-on is not installed"); + + // bad only max + apps = { + browser_specific_settings: { + gecko: { + id: addonId, + strict_max_version: "1", + }, + }, + }; + testManifest = Object.assign(apps, MANIFEST); + + extension = ExtensionTestUtils.loadExtension({ + manifest: testManifest, + useAddonManager: "temporary", + }); + + expectedMsg = new RegExp( + "Add-on strict_min_max@tests.mozilla.org is not compatible with application version. " + + "add-on maxVersion: 1." + ); + await Assert.rejects( + extension.startup(), + expectedMsg, + "Install rejects when specified minVersion and maxVersion are not valid" + ); + + addon = await promiseAddonByID(addonId); + equal(addon, null, "Add-on is not installed"); + + // good both + apps = { + browser_specific_settings: { + gecko: { + id: addonId, + strict_min_version: "1", + strict_max_version: "2", + }, + }, + }; + testManifest = Object.assign(apps, MANIFEST); + + extension = ExtensionTestUtils.loadExtension({ + manifest: testManifest, + useAddonManager: "temporary", + }); + + await extension.startup(); + addon = await promiseAddonByID(addonId); + + notEqual(addon, null, "Add-on is installed"); + equal(addon.id, addonId, "Installed add-on has the expected ID"); + await extension.unload(); + + // good only min + let newId = "strict_min_only@tests.mozilla.org"; + apps = { + browser_specific_settings: { + gecko: { + id: newId, + strict_min_version: "1", + }, + }, + }; + testManifest = Object.assign(apps, MANIFEST); + + extension = ExtensionTestUtils.loadExtension({ + manifest: testManifest, + useAddonManager: "temporary", + }); + + await extension.startup(); + addon = await promiseAddonByID(newId); + + notEqual(addon, null, "Add-on is installed"); + equal(addon.id, newId, "Installed add-on has the expected ID"); + + await extension.unload(); + + // good only max + newId = "strict_max_only@tests.mozilla.org"; + apps = { + browser_specific_settings: { + gecko: { + id: newId, + strict_max_version: "2", + }, + }, + }; + testManifest = Object.assign(apps, MANIFEST); + + extension = ExtensionTestUtils.loadExtension({ + manifest: testManifest, + useAddonManager: "temporary", + }); + + await extension.startup(); + addon = await promiseAddonByID(newId); + + notEqual(addon, null, "Add-on is installed"); + equal(addon.id, newId, "Installed add-on has the expected ID"); + + await extension.unload(); + + // * in min will throw an error + for (let version of ["0.*", "0.*.0"]) { + newId = "strict_min_star@tests.mozilla.org"; + let minStarApps = { + browser_specific_settings: { + gecko: { + id: newId, + strict_min_version: version, + }, + }, + }; + + let minStarTestManifest = Object.assign(minStarApps, MANIFEST); + + let minStarExtension = ExtensionTestUtils.loadExtension({ + manifest: minStarTestManifest, + useAddonManager: "temporary", + }); + + await Assert.rejects( + minStarExtension.startup(), + /The use of '\*' in strict_min_version is invalid/, + "loading an extension with a * in strict_min_version throws an exception" + ); + + let minStarAddon = await promiseAddonByID(newId); + equal(minStarAddon, null, "Add-on is not installed"); + } + + // incompatible extension but with compatibility checking off + newId = "checkCompatibility@tests.mozilla.org"; + apps = { + browser_specific_settings: { + gecko: { + id: newId, + strict_max_version: "1", + }, + }, + }; + testManifest = Object.assign(apps, MANIFEST); + + extension = ExtensionTestUtils.loadExtension({ + manifest: testManifest, + useAddonManager: "temporary", + }); + + let savedCheckCompatibilityValue = AddonManager.checkCompatibility; + AddonManager.checkCompatibility = false; + await extension.startup(); + addon = await promiseAddonByID(newId); + + notEqual(addon, null, "Add-on is installed"); + equal(addon.id, newId, "Installed add-on has the expected ID"); + + await extension.unload(); + AddonManager.checkCompatibility = savedCheckCompatibilityValue; +}); + +// Check permissions prompt +add_task(async function test_permissions_prompt() { + const manifest = { + name: "permissions test", + description: "permissions test", + manifest_version: 2, + version: "1.0", + + permissions: ["tabs", "storage", "https://*.example.com/*", "<all_urls>"], + }; + + let xpi = ExtensionTestCommon.generateXPI({ manifest }); + + let install = await AddonManager.getInstallForFile(xpi); + + let perminfo; + install.promptHandler = info => { + perminfo = info; + return Promise.resolve(); + }; + + await install.install(); + + notEqual(perminfo, undefined, "Permission handler was invoked"); + equal( + perminfo.existingAddon, + null, + "Permission info does not include an existing addon" + ); + notEqual(perminfo.addon, null, "Permission info includes the new addon"); + let perms = perminfo.addon.userPermissions; + deepEqual( + perms.permissions, + ["tabs", "storage"], + "API permissions are correct" + ); + deepEqual( + perms.origins, + ["https://*.example.com/*", "<all_urls>"], + "Host permissions are correct" + ); + + let addon = await promiseAddonByID(perminfo.addon.id); + notEqual(addon, null, "Extension was installed"); + + await addon.uninstall(); + await IOUtils.remove(xpi.path); +}); + +// Check permissions prompt cancellation +add_task(async function test_permissions_prompt_cancel() { + const manifest = { + name: "permissions test", + description: "permissions test", + manifest_version: 2, + version: "1.0", + + permissions: ["webRequestBlocking"], + }; + + let xpi = ExtensionTestCommon.generateXPI({ manifest }); + + let install = await AddonManager.getInstallForFile(xpi); + + let perminfo; + install.promptHandler = info => { + perminfo = info; + return Promise.reject(); + }; + + await promiseCompleteInstall(install); + + notEqual(perminfo, undefined, "Permission handler was invoked"); + + let addon = await promiseAddonByID(perminfo.addon.id); + equal(addon, null, "Extension was not installed"); + + await IOUtils.remove(xpi.path); +}); + +// Test that presence of 'edge' property in 'browser_specific_settings' doesn't prevent installation from completing successfully +add_task(async function test_non_gecko_bss_install() { + const ID = "ms_edge@tests.mozilla.org"; + + const manifest = { + name: "MS Edge and unknown browser test", + description: + "extension with bss properties for 'edge', and 'unknown_browser'", + manifest_version: 2, + version: "1.0", + applications: { gecko: { id: ID } }, + browser_specific_settings: { + edge: { + browser_action_next_to_addressbar: true, + }, + unknown_browser: { + unknown_setting: true, + }, + }, + }; + + const extension = ExtensionTestUtils.loadExtension({ + manifest, + useAddonManager: "temporary", + }); + ExtensionTestUtils.failOnSchemaWarnings(false); + await extension.startup(); + ExtensionTestUtils.failOnSchemaWarnings(true); + + const addon = await promiseAddonByID(ID); + notEqual(addon, null, "Add-on is installed"); + + await extension.unload(); +}); + +// Test that bss overrides applications if both are present. +add_task(async function test_duplicate_bss() { + const ID = "expected@tests.mozilla.org"; + + const manifest = { + manifest_version: 2, + version: "1.0", + applications: { + gecko: { id: "unexpected@tests.mozilla.org" }, + }, + browser_specific_settings: { + gecko: { id: ID }, + }, + }; + + const extension = ExtensionTestUtils.loadExtension({ + manifest, + useAddonManager: "temporary", + }); + ExtensionTestUtils.failOnSchemaWarnings(false); + await extension.startup(); + ExtensionTestUtils.failOnSchemaWarnings(true); + + const addon = await promiseAddonByID(ID); + notEqual(addon, null, "Add-on is installed"); + + await extension.unload(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install_syntax_error.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install_syntax_error.js new file mode 100644 index 0000000000..0edc6ec5a4 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install_syntax_error.js @@ -0,0 +1,42 @@ +const ADDON_ID = "webext-test@tests.mozilla.org"; + +add_task(async function setup() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + await promiseStartupManager(); +}); + +add_task(async function install_xpi() { + // WebExtension with a JSON syntax error in manifest.json + let xpi1 = AddonTestUtils.createTempWebExtensionFile({ + files: { + "manifest.json": String.raw`{ + "manifest_version: 2, + "browser_specific_settings": {"gecko": {"id": "${ADDON_ID}"}}, + "name": "Temp WebExt with Error", + "version": "0.1" + }`, + }, + }); + + // Valid WebExtension + let xpi2 = AddonTestUtils.createTempWebExtensionFile({ + files: { + "manifest.json": String.raw`{ + "manifest_version": 2, + "browser_specific_settings": {"gecko": {"id": "${ADDON_ID}"}}, + "name": "Temp WebExt without Error", + "version": "0.1" + }`, + }, + }); + + let install1 = await AddonManager.getInstallForFile(xpi1); + Assert.equal(install1.state, AddonManager.STATE_DOWNLOAD_FAILED); + Assert.equal(install1.error, AddonManager.ERROR_CORRUPT_FILE); + + // Replace xpi1 with xpi2 to have the same filename to reproduce install error + xpi2.moveTo(xpi1.parent, xpi1.leafName); + + let install2 = await AddonManager.getInstallForFile(xpi2); + Assert.equal(install2.error, 0); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_langpack.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_langpack.js new file mode 100644 index 0000000000..cb70a3910f --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_langpack.js @@ -0,0 +1,669 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +const { ExtensionUtils } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionUtils.sys.mjs" +); + +ChromeUtils.defineLazyGetter(this, "resourceProtocol", () => + Services.io + .getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler) +); + +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); + +const ID = "langpack-und@test.mozilla.org"; + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +// Langpacks versions follow the following convention: +// <firefox major>.<firefox minor>.YYYYMMDD.HHmmss +// with no leading zeros allowed (as enforced per version format, see MDN doc page at https://mzl.la/3M6L15y). +// +// See https://searchfox.org/mozilla-central/rev/26790fe/python/mozbuild/mozbuild/action/langpack_manifest.py#388-398 +var server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); +AddonTestUtils.registerJSON(server, "/test_update_langpack.json", { + addons: { + "langpack-und@test.mozilla.org": { + updates: [ + { + version: "58.0.20230105.121014", + applications: { + gecko: { + strict_min_version: "58.0", + strict_max_version: "58.*", + }, + }, + }, + { + version: "60.0.20230207.112555", + update_link: + "http://example.com/addons/langpack-und@test.mozilla.org.xpi", + applications: { + gecko: { + strict_min_version: "60.0", + strict_max_version: "60.*", + }, + }, + }, + { + version: "60.1.20230309.91233", + update_link: + "http://example.com/addons/dotrelease/langpack-und@test.mozilla.org.xpi", + applications: { + gecko: { + strict_min_version: "60.0", + strict_max_version: "60.*", + }, + }, + }, + ], + }, + }, +}); + +// A second update url, which is included in the last of the langpack +// version from the previous one (and used to cover the staging of a +// langpack from one dotrelease to another). +AddonTestUtils.registerJSON(server, "/test_update_langpack2.json", { + addons: { + "langpack-und@test.mozilla.org": { + updates: [ + { + version: "60.2.20230319.94511", + update_link: + "http://example.com/addons/dotrelease2/langpack-und@test.mozilla.org.xpi", + applications: { + gecko: { + strict_min_version: "60.0", + strict_max_version: "60.*", + }, + }, + }, + ], + }, + }, +}); + +function promisePostponeInstall(install) { + return new Promise((resolve, reject) => { + let listener = { + onDownloadEnded: () => { + install.postpone(); + }, + onInstallFailed: () => { + install.removeListener(listener); + reject(new Error("extension installation should not have failed")); + }, + onInstallEnded: () => { + install.removeListener(listener); + reject( + new Error( + `extension installation should not have ended for ${install.addon.id}` + ) + ); + }, + onInstallPostponed: () => { + install.removeListener(listener); + resolve(); + }, + }; + + install.addListener(listener); + }); +} + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "58"); + +const ADDONS = { + langpack_1: { + "browser/localization/und/browser.ftl": + "message-browser = Value from Browser\n", + "localization/und/toolkit_test.ftl": "message-id1 = Value 1\n", + "chrome/und/locale/und/global/test.properties": + "message = Value from .properties\n", + "manifest.json": { + name: "und Language Pack", + version: "58.0.20230105.121014", + manifest_version: 2, + browser_specific_settings: { + gecko: { + id: ID, + strict_min_version: "58.0", + strict_max_version: "58.*", + update_url: "http://example.com/test_update_langpack.json", + }, + }, + sources: { + browser: { + base_path: "browser/", + }, + }, + langpack_id: "und", + languages: { + und: { + chrome_resources: { + global: "chrome/und/locale/und/global/", + }, + version: "20171001190118", + }, + }, + author: "Mozilla Localization Task Force", + description: "Language pack for Testy for und", + }, + }, +}; + +// clone the extension so we can create an update. +const langpack_update = JSON.parse(JSON.stringify(ADDONS.langpack_1)); +langpack_update["manifest.json"].version = "60.0.20230207.112555"; +langpack_update["manifest.json"].browser_specific_settings.gecko = { + id: ID, + strict_min_version: "60.0", + strict_max_version: "60.*", + update_url: "http://example.com/test_update_langpack.json", +}; + +const langpack_update_dotrelease = JSON.parse( + JSON.stringify(ADDONS.langpack_1) +); +langpack_update_dotrelease["manifest.json"].version = "60.1.20230309.91233"; +langpack_update_dotrelease["manifest.json"].browser_specific_settings.gecko = { + id: ID, + strict_min_version: "60.0", + strict_max_version: "60.*", + update_url: "http://example.com/test_update_langpack2.json", +}; + +// Another langpack for another dot release part of the same major version as the previous one. +const langpack_update_dotrelease2 = JSON.parse( + JSON.stringify(ADDONS.langpack_1) +); +langpack_update_dotrelease2["manifest.json"].version = "60.2.20230319.94511"; +langpack_update_dotrelease2["manifest.json"].browser_specific_settings.gecko = { + id: ID, + strict_min_version: "60.0", + strict_max_version: "60.*", + update_url: "http://example.com/test_update_langpack2.json", +}; + +let xpi = AddonTestUtils.createTempXPIFile(langpack_update); +server.registerFile(`/addons/${ID}.xpi`, xpi); + +let xpiDotRelease = AddonTestUtils.createTempXPIFile( + langpack_update_dotrelease +); +server.registerFile(`/addons/dotrelease/${ID}.xpi`, xpiDotRelease); + +let xpiDotRelease2 = AddonTestUtils.createTempXPIFile( + langpack_update_dotrelease2 +); +server.registerFile(`/addons/dotrelease2/${ID}.xpi`, xpiDotRelease2); + +function promiseLangpackStartup() { + return new Promise(resolve => { + const EVENT = "webextension-langpack-startup"; + Services.obs.addObserver(function observer() { + Services.obs.removeObserver(observer, EVENT); + resolve(); + }, EVENT); + }); +} + +add_task(async function setup() { + Services.prefs.clearUserPref("extensions.startupScanScopes"); +}); + +/** + * This is a basic life-cycle test which verifies that + * the language pack registers and unregisters correct + * languages at various stages. + */ +add_task(async function test_basic_lifecycle() { + await promiseStartupManager(); + + // Make sure that `und` locale is not installed. + equal( + L10nRegistry.getInstance().getAvailableLocales().includes("und"), + false, + "und not installed" + ); + equal( + Services.locale.availableLocales.includes("und"), + false, + "und not available" + ); + + let [, { addon }] = await Promise.all([ + promiseLangpackStartup(), + AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1), + ]); + + // Now make sure that `und` locale is available. + equal( + L10nRegistry.getInstance().getAvailableLocales().includes("und"), + true, + "und is installed" + ); + equal( + Services.locale.availableLocales.includes("und"), + true, + "und is available" + ); + + await addon.disable(); + + // It is not available after the langpack has been disabled. + equal( + L10nRegistry.getInstance().getAvailableLocales().includes("und"), + false, + "und not installed" + ); + equal( + Services.locale.availableLocales.includes("und"), + false, + "und not available" + ); + + // This quirky code here allows us to handle a scenario where enabling the + // addon is synchronous or asynchronous. + await Promise.all([promiseLangpackStartup(), addon.enable()]); + + // After re-enabling it, the `und` locale is available again. + equal( + L10nRegistry.getInstance().getAvailableLocales().includes("und"), + true, + "und is installed" + ); + equal( + Services.locale.availableLocales.includes("und"), + true, + "und is available" + ); + + await addon.uninstall(); + + // After the langpack has been uninstalled, no more `und` in locales. + equal( + L10nRegistry.getInstance().getAvailableLocales().includes("und"), + false, + "und not installed" + ); + equal( + Services.locale.availableLocales.includes("und"), + false, + "und not available" + ); +}); + +/** + * This test verifies that registries are able to load and synchronously return + * correct strings available in the language pack. + */ +add_task(async function test_locale_registries() { + let [, { addon }] = await Promise.all([ + promiseLangpackStartup(), + AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1), + ]); + + { + // Toolkit string + let bundles = L10nRegistry.getInstance().generateBundlesSync( + ["und"], + ["toolkit_test.ftl"] + ); + let bundle0 = bundles.next().value; + ok(bundle0); + equal(bundle0.hasMessage("message-id1"), true); + } + + { + // Browser string + let bundles = L10nRegistry.getInstance().generateBundlesSync( + ["und"], + ["browser.ftl"] + ); + let bundle0 = bundles.next().value; + ok(bundle0); + equal(bundle0.hasMessage("message-browser"), true); + } + + { + // Test chrome package + let reqLocs = Services.locale.requestedLocales; + Services.locale.requestedLocales = ["und"]; + + let bundle = Services.strings.createBundle( + "chrome://global/locale/test.properties" + ); + let entry = bundle.GetStringFromName("message"); + equal(entry, "Value from .properties"); + + Services.locale.requestedLocales = reqLocs; + } + + await addon.uninstall(); +}); + +/** + * This test verifies that registries are able to load and asynchronously return + * correct strings available in the language pack. + */ +add_task(async function test_locale_registries_async() { + let [, { addon }] = await Promise.all([ + promiseLangpackStartup(), + AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1), + ]); + + { + // Toolkit string + let bundles = L10nRegistry.getInstance().generateBundles( + ["und"], + ["toolkit_test.ftl"] + ); + let bundle0 = (await bundles.next()).value; + equal(bundle0.hasMessage("message-id1"), true); + } + + { + // Browser string + let bundles = L10nRegistry.getInstance().generateBundles( + ["und"], + ["browser.ftl"] + ); + let bundle0 = (await bundles.next()).value; + equal(bundle0.hasMessage("message-browser"), true); + } + + await addon.uninstall(); + await promiseShutdownManager(); +}); + +add_task(async function test_langpack_app_shutdown() { + let langpackId = `langpack-und-${AppConstants.MOZ_BUILD_APP.replace( + "/", + "-" + )}`; + let check = (yes, msg) => { + equal(resourceProtocol.hasSubstitution(langpackId), yes, msg); + }; + + await promiseStartupManager(); + + check(false, "no initial resource substitution"); + + await Promise.all([ + promiseLangpackStartup(), + AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1), + ]); + + check(true, "langpack resource available after startup"); + + await promiseShutdownManager(); + + check(true, "langpack resource available after app shutdown"); + + await promiseStartupManager(); + + let addon = await AddonManager.getAddonByID(ID); + await addon.uninstall(); + + check(false, "langpack resource removed during shutdown for uninstall"); + + await promiseShutdownManager(); +}); + +add_task(async function test_amazing_disappearing_langpacks() { + let check = yes => { + equal( + L10nRegistry.getInstance().getAvailableLocales().includes("und"), + yes, + "check L10nRegistry" + ); + equal( + Services.locale.availableLocales.includes("und"), + yes, + "check availableLocales" + ); + }; + + await promiseStartupManager(); + + check(false); + + await Promise.all([ + promiseLangpackStartup(), + AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1), + ]); + + check(true); + + await promiseShutdownManager(); + + check(false); + + await AddonTestUtils.manuallyUninstall(AddonTestUtils.profileExtensions, ID); + + await promiseStartupManager(); + + check(false); +}); + +/** + * This test verifies that language pack will get disabled after app + * gets upgraded. + */ +add_task(async function test_disable_after_app_update() { + let [, { addon }] = await Promise.all([ + promiseLangpackStartup(), + AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1), + ]); + Assert.ok(addon.isActive); + + await promiseRestartManager("59"); + + addon = await promiseAddonByID(ID); + Assert.ok(!addon.isActive); + Assert.ok(addon.appDisabled); + + await addon.uninstall(); + await promiseShutdownManager(); +}); + +/** + * This test verifies that a postponed language pack update will be + * applied after a restart. + */ +add_task(async function test_after_app_update() { + await promiseStartupManager("58"); + let [, { addon }] = await Promise.all([ + promiseLangpackStartup(), + AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1), + ]); + Assert.ok(addon.isActive); + + await promiseRestartManager("60"); + + addon = await promiseAddonByID(ID); + Assert.ok(!addon.isActive); + Assert.ok(addon.appDisabled); + Assert.equal(addon.version, "58.0.20230105.121014"); + + let update = await promiseFindAddonUpdates(addon); + Assert.ok(update.updateAvailable, "update is available"); + let install = update.updateAvailable; + let postponed = promisePostponeInstall(install); + install.install(); + await postponed; + Assert.equal( + install.state, + AddonManager.STATE_POSTPONED, + "install postponed" + ); + + await promiseRestartManager(); + + addon = await promiseAddonByID(ID); + Assert.ok(addon.isActive); + Assert.equal(addon.version, "60.1.20230309.91233"); + + await addon.uninstall(); + await promiseShutdownManager(); +}); + +// Support setting the request locale. +function promiseLocaleChanged(requestedLocales) { + let changed = ExtensionUtils.promiseObserved( + "intl:requested-locales-changed" + ); + Services.locale.requestedLocales = requestedLocales; + return changed; +} + +/** + * This test verifies that an addon update for the next version can be + * retrieved and staged for restart. + */ +add_task(async function test_staged_langpack_for_app_update() { + let originalLocales = Services.locale.requestedLocales; + + await promiseStartupManager("58"); + let [, { addon }] = await Promise.all([ + promiseLangpackStartup(), + AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1), + ]); + Assert.ok(addon.isActive); + await promiseLocaleChanged(["und"]); + + // Mimick a major release update happening while a + // langpack from a dotrelease what already available + // (and then assert that the dotrelease langpack + // is the one staged and then installed on browser + // restart) + await AddonManager.stageLangpacksForAppUpdate("60"); + await promiseRestartManager("60"); + + addon = await promiseAddonByID(ID); + Assert.ok(addon.isActive); + Assert.equal(addon.version, "60.1.20230309.91233"); + + // Mimick a second dotrelease update, along with + // staging of the langpacks released along with it + // (then assert that the langpack for the second + // dotrelease is staged and then installed on + // browser restart). + await promiseRestartManager("60.1"); + await AddonManager.stageLangpacksForAppUpdate("60.2"); + await promiseRestartManager("60.2"); + + addon = await promiseAddonByID(ID); + Assert.ok(addon.isActive); + Assert.equal(addon.version, "60.2.20230319.94511"); + + await addon.uninstall(); + await promiseShutdownManager(); + + Services.locale.requestedLocales = originalLocales; +}); + +/** + * This test verifies that an addon update for the next version can be + * retrieved and staged for restart, but a restart failure falls back. + */ +add_task(async function test_staged_langpack_for_app_update_fail() { + let originalLocales = Services.locale.requestedLocales; + + await promiseStartupManager("58"); + let [, { addon }] = await Promise.all([ + promiseLangpackStartup(), + AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1), + ]); + Assert.ok(addon.isActive); + await promiseLocaleChanged(["und"]); + + await AddonManager.stageLangpacksForAppUpdate("60"); + await promiseRestartManager(); + + addon = await promiseAddonByID(ID); + Assert.ok(addon.isActive); + Assert.equal(addon.version, "58.0.20230105.121014"); + + await addon.uninstall(); + await promiseShutdownManager(); + Services.locale.requestedLocales = originalLocales; +}); + +/** + * This test verifies that an update restart works when the langpack + * cannot be updated. + */ +add_task(async function test_staged_langpack_for_app_update_not_found() { + let originalLocales = Services.locale.requestedLocales; + + await promiseStartupManager("58"); + let [, { addon }] = await Promise.all([ + promiseLangpackStartup(), + AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1), + ]); + Assert.ok(addon.isActive); + await promiseLocaleChanged(["und"]); + + await AddonManager.stageLangpacksForAppUpdate("59"); + await promiseRestartManager("59"); + + addon = await promiseAddonByID(ID); + Assert.ok(!addon.isActive); + Assert.equal(addon.version, "58.0.20230105.121014"); + + await addon.uninstall(); + await promiseShutdownManager(); + Services.locale.requestedLocales = originalLocales; +}); + +/** + * This test verifies that a compat update with an invalid max_version + * will be disabled, at least allowing Firefox to startup without failures. + */ +add_task(async function test_staged_langpack_compat_startup() { + let originalLocales = Services.locale.requestedLocales; + + await promiseStartupManager("58"); + let [, { addon }] = await Promise.all([ + promiseLangpackStartup(), + AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1), + ]); + Assert.ok(addon.isActive); + await promiseLocaleChanged(["und"]); + + // Mimick a compatibility update + let compatUpdate = { + targetApplications: [ + { + id: "toolkit@mozilla.org", + minVersion: "58", + maxVersion: "*", + }, + ], + }; + addon.__AddonInternal__.applyCompatibilityUpdate(compatUpdate); + + await promiseRestartManager("59"); + + addon = await promiseAddonByID(ID); + Assert.ok(!addon.isActive, "addon is not active after upgrade"); + ok(!addon.isCompatible, "compatibility update fixed"); + + await promiseRestartManager("58"); + + addon = await promiseAddonByID(ID); + Assert.ok(addon.isActive, "addon is active after downgrade"); + ok(addon.isCompatible, "compatibility update fixed"); + + await addon.uninstall(); + await promiseShutdownManager(); + Services.locale.requestedLocales = originalLocales; +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_paths.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_paths.js new file mode 100644 index 0000000000..564e9086ac --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_paths.js @@ -0,0 +1,47 @@ +let profileDir; +add_task(async function setup() { + profileDir = gProfD.clone(); + profileDir.append("extensions"); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + await promiseStartupManager(); +}); + +// When installing an unpacked addon we derive the ID from the +// directory name. Make sure that if the directory name is not a valid +// addon ID that we reject it. +add_task(async function test_bad_unpacked_path() { + let MANIFEST_ID = "webext_bad_path@tests.mozilla.org"; + + let manifest = { + name: "path test", + description: "test of a bad directory name", + manifest_version: 2, + version: "1.0", + + browser_specific_settings: { + gecko: { + id: MANIFEST_ID, + }, + }, + }; + + const directories = ["not a valid ID", '"quotes"@tests.mozilla.org']; + + for (let dir of directories) { + try { + await promiseWriteWebManifestForExtension(manifest, profileDir, dir); + } catch (ex) { + // This can fail if the underlying filesystem (looking at you windows) + // doesn't handle some of the characters in the ID. In that case, + // just ignore this test on this platform. + continue; + } + await promiseRestartManager(); + + let addon = await promiseAddonByID(dir); + Assert.equal(addon, null); + addon = await promiseAddonByID(MANIFEST_ID); + Assert.equal(addon, null); + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_theme.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_theme.js new file mode 100644 index 0000000000..8e8e79c2c7 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_theme.js @@ -0,0 +1,365 @@ +"use strict"; + +/** + * This file contains test for 'theme' type WebExtension addons. Tests focus mostly + * on interoperability between the different theme formats (XUL and LWT) and + * Addon Manager integration. + * + * Coverage may overlap with other tests in this folder. + */ + +const THEME_IDS = [ + "theme3@tests.mozilla.org", + "theme2@personas.mozilla.org", // Unused. Legacy. Evil. + "default-theme@mozilla.org", +]; +const REAL_THEME_IDS = [THEME_IDS[0], THEME_IDS[2]]; +const DEFAULT_THEME = THEME_IDS[2]; + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +Services.prefs.setIntPref( + "extensions.enabledScopes", + AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION +); + +// We remember the last/ currently active theme for tracking events. +var gActiveTheme = null; + +add_task(async function setup_to_default_browserish_state() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + + await promiseWriteWebManifestForExtension( + { + author: "Some author", + manifest_version: 2, + name: "Web Extension Name", + version: "1.0", + theme: { images: { theme_frame: "example.png" } }, + browser_specific_settings: { + gecko: { + id: THEME_IDS[0], + }, + }, + }, + profileDir + ); + + await promiseStartupManager(); + + if (AppConstants.MOZ_DEV_EDITION) { + // Developer Edition selects the wrong theme by default. + let defaultTheme = await AddonManager.getAddonByID(DEFAULT_THEME); + await defaultTheme.enable(); + } + + let [t1, t2, d] = await promiseAddonsByIDs(THEME_IDS); + Assert.ok(t1, "Theme addon should exist"); + Assert.equal(t2, null, "Theme addon is not a thing anymore"); + Assert.ok(d, "Theme addon should exist"); + + await t1.disable(); + await new Promise(executeSoon); + Assert.ok(!t1.isActive, "Theme should be disabled"); + Assert.ok(d.isActive, "Default theme should be active"); + + await promiseRestartManager(); + + [t1, t2, d] = await promiseAddonsByIDs(THEME_IDS); + Assert.ok(!t1.isActive, "Theme should still be disabled"); + Assert.ok(d.isActive, "Default theme should still be active"); + + gActiveTheme = d.id; +}); + +/** + * Set the `userDisabled` property of one specific theme and check if the theme + * switching works as expected by checking the state of all installed themes. + * + * @param {String} which ID of the addon to set the `userDisabled` property on + * @param {Boolean} disabled Flag value to switch to + */ +async function setDisabledStateAndCheck(which, disabled = false) { + if (disabled) { + Assert.equal(which, gActiveTheme, "Only the active theme can be disabled"); + } + + let themeToDisable = disabled ? which : gActiveTheme; + let themeToEnable = disabled ? DEFAULT_THEME : which; + + let expectedStates = { + [themeToDisable]: true, + [themeToEnable]: false, + }; + let addonEvents = { + [themeToDisable]: [{ event: "onDisabling" }, { event: "onDisabled" }], + [themeToEnable]: [{ event: "onEnabling" }, { event: "onEnabled" }], + }; + + // Set the state of the theme to change. + let theme = await promiseAddonByID(which); + await expectEvents({ addonEvents }, () => { + if (disabled) { + theme.disable(); + } else { + theme.enable(); + } + }); + + let isDisabled; + for (theme of await promiseAddonsByIDs(REAL_THEME_IDS)) { + isDisabled = theme.id in expectedStates ? expectedStates[theme.id] : true; + Assert.equal( + theme.userDisabled, + isDisabled, + `Theme '${theme.id}' should be ${isDisabled ? "dis" : "en"}abled` + ); + Assert.equal( + theme.pendingOperations, + AddonManager.PENDING_NONE, + "There should be no pending operations when no restart is expected" + ); + Assert.equal( + theme.isActive, + !isDisabled, + `Theme '${theme.id} should be ${isDisabled ? "in" : ""}active` + ); + } + + await promiseRestartManager(); + + // All should still be good after a restart of the Addon Manager. + for (theme of await promiseAddonsByIDs(REAL_THEME_IDS)) { + isDisabled = theme.id in expectedStates ? expectedStates[theme.id] : true; + Assert.equal( + theme.userDisabled, + isDisabled, + `Theme '${theme.id}' should be ${isDisabled ? "dis" : "en"}abled` + ); + Assert.equal( + theme.isActive, + !isDisabled, + `Theme '${theme.id}' should be ${isDisabled ? "in" : ""}active` + ); + Assert.equal( + theme.pendingOperations, + AddonManager.PENDING_NONE, + "There should be no pending operations left" + ); + if (!isDisabled) { + gActiveTheme = theme.id; + } + } +} + +add_task(async function test_WebExtension_themes() { + // Enable the WebExtension theme. + await setDisabledStateAndCheck(THEME_IDS[0]); + + // Disabling WebExtension should revert to the default theme. + await setDisabledStateAndCheck(THEME_IDS[0], true); + + // Enable it again. + await setDisabledStateAndCheck(THEME_IDS[0]); +}); + +add_task(async function test_default_theme() { + // Explicitly enable the default theme. + await setDisabledStateAndCheck(DEFAULT_THEME); + + // Swith to the WebExtension theme. + await setDisabledStateAndCheck(THEME_IDS[0]); + + // Enable it again. + await setDisabledStateAndCheck(DEFAULT_THEME); +}); + +add_task(async function uninstall_offers_undo() { + let defaultTheme = await AddonManager.getAddonByID(DEFAULT_THEME); + const ID = THEME_IDS[0]; + let theme = await promiseAddonByID(ID); + + Assert.ok(theme, "Webextension theme is present"); + + async function promiseAddonEvent(event, id) { + let [addon] = await AddonTestUtils.promiseAddonEvent(event); + if (id) { + Assert.equal(addon.id, id, `Got event for expected addon (${event})`); + } + } + + async function uninstallTheme() { + let uninstallingPromise = promiseAddonEvent("onUninstalling", ID); + await theme.uninstall(true); + await uninstallingPromise; + + Assert.ok( + hasFlag(theme.pendingOperations, AddonManager.PENDING_UNINSTALL), + "Theme being uninstalled has PENDING_UNINSTALL flag" + ); + } + + async function cancelUninstallTheme() { + let cancelPromise = promiseAddonEvent("onOperationCancelled", ID); + theme.cancelUninstall(); + await cancelPromise; + + Assert.equal( + theme.pendingOperations, + AddonManager.PENDING_NONE, + "PENDING_UNINSTALL flag is cleared when uninstall is canceled" + ); + } + + // A theme should still be disabled if the uninstallation of a disabled theme + // is undone. + Assert.ok(!theme.isActive, "Webextension theme is not active"); + Assert.ok(defaultTheme.isActive, "Default theme is active"); + await uninstallTheme(); + await cancelUninstallTheme(); + Assert.ok(!theme.isActive, "Webextension theme is still not active"); + Assert.ok(defaultTheme.isActive, "Default theme is still active"); + + // Enable theme, the previously active theme should be disabled. + await Promise.all([ + promiseAddonEvent("onDisabled", DEFAULT_THEME), + promiseAddonEvent("onEnabled", ID), + theme.enable(), + ]); + Assert.ok(theme.isActive, "Webextension theme is active after enabling"); + Assert.ok(!defaultTheme.isActive, "Default theme is not active any more"); + + // Uninstall active theme, default theme should become active. + await Promise.all([ + // Note: no listener for onDisabled & ID because the uninstall is pending. + promiseAddonEvent("onEnabled", DEFAULT_THEME), + uninstallTheme(), + ]); + Assert.ok(!theme.isActive, "Webextension theme is not active upon uninstall"); + Assert.ok(defaultTheme.isActive, "Default theme is active again"); + + // Undo uninstall, default theme should be deactivated. + await Promise.all([ + // Note: no listener for onEnabled & ID because the uninstall was pending. + promiseAddonEvent("onDisabled", DEFAULT_THEME), + cancelUninstallTheme(), + ]); + Assert.ok(theme.isActive, "Webextension theme is active upon undo uninstall"); + Assert.ok(!defaultTheme.isActive, "Default theme is not active again"); + + // Immediately remove the theme. Default theme should be activated. + await Promise.all([ + promiseAddonEvent("onEnabled", DEFAULT_THEME), + theme.uninstall(), + ]); + + await promiseRestartManager(); +}); + +// Test that default_locale works with WE themes +add_task(async function default_locale_themes() { + let addon = await promiseInstallWebExtension({ + manifest: { + default_locale: "en", + name: "__MSG_name__", + description: "__MSG_description__", + theme: { + colors: { + frame: "black", + tab_background_text: "white", + }, + }, + }, + files: { + "_locales/en/messages.json": `{ + "name": { + "message": "the name" + }, + "description": { + "message": "the description" + } + }`, + }, + }); + + addon = await promiseAddonByID(addon.id); + equal(addon.name, "the name"); + equal(addon.description, "the description"); + equal(addon.type, "theme"); + await addon.uninstall(); +}); + +add_task(async function test_theme_update() { + let addon = await AddonManager.getAddonByID(DEFAULT_THEME); + ok(!addon.userDisabled, "default theme is enabled"); + + await AddonTestUtils.promiseRestartManager("2"); + + addon = await AddonManager.getAddonByID(DEFAULT_THEME); + ok(!addon.userDisabled, "default theme is enabled after upgrade"); +}); + +add_task(async function test_builtin_theme_permissions() { + const ADDON_ID = "mytheme@mozilla.org"; + + let themeDef = { + manifest: { + browser_specific_settings: { gecko: { id: ADDON_ID } }, + version: "1.0", + theme: {}, + }, + }; + + function checkPerms(addon) { + // builtin themes enable or disable based on disabled state + Assert.equal( + addon.userDisabled, + hasFlag(addon.permissions, AddonManager.PERM_CAN_ENABLE), + "enable permission is correct" + ); + Assert.equal( + !addon.userDisabled, + hasFlag(addon.permissions, AddonManager.PERM_CAN_DISABLE), + "disable permission is correct" + ); + // builtin themes do not get any other permission + Assert.ok( + !hasFlag(addon.permissions, AddonManager.PERM_CAN_INSTALL), + "cannot install by user" + ); + Assert.ok( + !hasFlag(addon.permissions, AddonManager.PERM_CAN_UPGRADE), + "cannot upgrade" + ); + Assert.ok( + !hasFlag(addon.permissions, AddonManager.PERM_CAN_UNINSTALL), + "cannot uninstall" + ); + Assert.ok( + !hasFlag( + addon.permissions, + AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS + ), + "can change private browsing access" + ); + Assert.ok( + hasFlag(addon.permissions, AddonManager.PERM_API_CAN_UNINSTALL), + "can uninstall via API" + ); + } + + await setupBuiltinExtension(themeDef, "first-loc", false); + await AddonManager.maybeInstallBuiltinAddon( + ADDON_ID, + "1.0", + "resource://first-loc/" + ); + + let addon = await AddonManager.getAddonByID(ADDON_ID); + checkPerms(addon); + await addon.enable(); + checkPerms(addon); + + await addon.uninstall(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-unpack.toml b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-unpack.toml new file mode 100644 index 0000000000..34eb268cc0 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-unpack.toml @@ -0,0 +1,15 @@ +[DEFAULT] +head = "head_addons.js head_unpack.js" +firefox-appdir = "browser" +skip-if = ["os == 'android'"] +dupe-manifest = true +tags = "addons" + +["test_filepointer.js"] +skip-if = [ + "!allow_legacy_extensions", + "require_signing", +] + +["test_webextension_paths.js"] +tags = "webextensions" diff --git a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.toml b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..6b1cb010d4 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.toml @@ -0,0 +1,362 @@ +[DEFAULT] +skip-if = ["os == 'android'"] +tags = "addons" +head = "head_addons.js" +firefox-appdir = "browser" +dupe-manifest = true +support-files = ["data/**"] + +["test_AMBrowserExtensionsImport.js"] + +["test_AbuseReporter.js"] + +["test_AddonRepository.js"] + +["test_AddonRepository_appIsShuttingDown.js"] + +["test_AddonRepository_cache.js"] + +["test_AddonRepository_cache_locale.js"] + +["test_AddonRepository_langpacks.js"] + +["test_AddonRepository_paging.js"] + +["test_ProductAddonChecker.js"] + +["test_ProductAddonChecker_signatures.js"] +head = "head_addons.js head_cert_handling.js" + +["test_QuarantinedDomains_AMRemoteSettings.js"] +head = "head_addons.js head_amremotesettings.js ../../../../components/extensions/test/xpcshell/head_telemetry.js" + +["test_QuarantinedDomains_AddonWrapper.js"] + +["test_XPIStates.js"] +skip-if = ["condprof"] # Bug 1769184 - by design for now + +["test_XPIcancel.js"] + +["test_addonStartup.js"] + +["test_addon_manager_telemetry_events.js"] + +["test_amo_stats_telemetry.js"] + +["test_aom_startup.js"] + +["test_bad_json.js"] + +["test_badschema.js"] + +["test_builtin_location.js"] + +["test_cacheflush.js"] + +["test_childprocess.js"] +head = "" + +["test_colorways_builtin_theme_upgrades.js"] +skip-if = ["appname == 'thunderbird'"] # Bug 1809438 - No colorways in Thunderbird + +["test_cookies.js"] + +["test_corrupt.js"] + +["test_crash_annotation_quoting.js"] + +["test_db_path.js"] +head = "" + +["test_delay_update_webextension.js"] +tags = "webextensions" + +["test_dependencies.js"] + +["test_dictionary_webextension.js"] + +["test_distribution.js"] + +["test_distribution_langpack.js"] + +["test_embedderDisabled.js"] + +["test_error.js"] +skip-if = ["os == 'win'"] # Bug 1508482 + +["test_ext_management.js"] +tags = "webextensions" + +["test_general.js"] + +["test_getInstallSourceFromHost.js"] + +["test_gmpProvider.js"] +skip-if = ["appname != 'firefox'"] + +["test_harness.js"] + +["test_hidden.js"] + +["test_install.js"] + +["test_installOrigins.js"] + +["test_install_cancel.js"] + +["test_install_file_change.js"] + +["test_install_icons.js"] + +["test_installtrigger_deprecation.js"] +head = "head_addons.js head_amremotesettings.js" + +["test_installtrigger_schemes.js"] + +["test_isDebuggable.js"] + +["test_isReady.js"] + +["test_loadManifest_isPrivileged.js"] + +["test_locale.js"] + +["test_moved_extension_metadata.js"] +skip-if = ["true"] # bug 1777900 + +["test_no_addons.js"] + +["test_nodisable_hidden.js"] + +["test_onPropertyChanged_appDisabled.js"] +head = "head_addons.js head_compat.js" +skip-if = ["tsan"] # Times out, bug 1674773 + +["test_permissions.js"] + +["test_permissions_prefs.js"] + +["test_pref_properties.js"] + +["test_provider_markSafe.js"] + +["test_provider_shutdown.js"] + +["test_provider_unsafe_access_shutdown.js"] + +["test_provider_unsafe_access_startup.js"] + +["test_proxies.js"] +skip-if = ["require_signing"] + +["test_recommendations.js"] +skip-if = ["require_signing"] + +["test_registerchrome.js"] + +["test_registry.js"] +run-if = ["os == 'win'"] + +["test_reinstall_disabled_addon.js"] + +["test_reload.js"] +skip-if = ["os == 'win'"] # There's a problem removing a temp file without manually clearing the cache on Windows +tags = "webextensions" + +["test_remote_pref_telemetry.js"] + +["test_safemode.js"] + +["test_schema_change.js"] + +["test_seen.js"] + +["test_shutdown.js"] + +["test_shutdown_barriers.js"] + +["test_shutdown_early.js"] +skip-if = ["condprof"] # Bug 1769184 - by design for now + +["test_sideload_scopes.js"] +head = "head_addons.js head_sideload.js" +skip-if = [ + "os == 'linux'", # Bug 1613268 + "condprof", # Bug 1769184 - by design for now +] + +["test_sideloads.js"] + +["test_sideloads_after_rebuild.js"] +skip-if = ["condprof"] # Bug 1769184 - by design for now +head = "head_addons.js head_sideload.js" + +["test_signed_inject.js"] +skip-if = ["true"] # Bug 1394122 + +["test_signed_install.js"] + +["test_signed_langpack.js"] + +["test_signed_long.js"] + +["test_signed_updatepref.js"] +skip-if = [ + "require_signing", + "!allow_legacy_extensions", +] + +["test_signed_verify.js"] + +["test_sitePermsAddonProvider.js"] +skip-if = ["appname == 'thunderbird'"] # Disabled in extensions.manifest + +["test_startup.js"] +head = "head_addons.js head_sideload.js" +skip-if = [ + "os == 'linux'", # Bug 1613268 + "condprof", # Bug 1769184 - by design for now +] + +["test_startup_enable.js"] + +["test_startup_isPrivileged.js"] + +["test_startup_scan.js"] +head = "head_addons.js head_sideload.js" + +["test_strictcompatibility.js"] +head = "head_addons.js head_compat.js" + +["test_syncGUID.js"] + +["test_system_allowed.js"] +head = "head_addons.js head_system_addons.js" + +["test_system_delay_update.js"] +head = "head_addons.js head_system_addons.js" + +["test_system_profile_location.js"] + +["test_system_repository.js"] +head = "head_addons.js head_system_addons.js" + +["test_system_reset.js"] +head = "head_addons.js head_system_addons.js" + +["test_system_update_blank.js"] +head = "head_addons.js head_system_addons.js" + +["test_system_update_checkSizeHash.js"] +head = "head_addons.js head_system_addons.js" + +["test_system_update_custom.js"] +head = "head_addons.js head_system_addons.js" + +["test_system_update_empty.js"] +head = "head_addons.js head_system_addons.js" +skip-if = ["true"] # Failing intermittently due to a race condition in the test, see bug 1348981 + +["test_system_update_enterprisepolicy.js"] +head = "head_addons.js head_system_addons.js" + +["test_system_update_fail.js"] +head = "head_addons.js head_system_addons.js" + +["test_system_update_installTelemetryInfo.js"] +head = "head_addons.js head_system_addons.js" + +["test_system_update_newset.js"] +head = "head_addons.js head_system_addons.js" + +["test_system_update_overlapping.js"] +head = "head_addons.js head_system_addons.js" + +["test_system_update_uninstall_check.js"] +head = "head_addons.js head_system_addons.js" + +["test_system_update_upgrades.js"] +head = "head_addons.js head_system_addons.js" + +["test_system_upgrades.js"] +skip-if = ["condprof"] # Bug 1769184 - by design for now +head = "head_addons.js head_system_addons.js" + +["test_systemaddomstartupprefs.js"] +skip-if = ["condprof"] # Bug 1769184 - by design for now +head = "head_addons.js head_system_addons.js" + +["test_temporary.js"] +skip-if = ["os == 'win'"] # Bug 1469904 +tags = "webextensions" + +["test_trash_directory.js"] +run-if = ["os == 'win'"] + +["test_types.js"] + +["test_undouninstall.js"] +skip-if = ["os == 'win'"] # Bug 1358846 + +["test_update.js"] + +["test_updateCancel.js"] + +["test_update_addontype.js"] + +["test_update_compatmode.js"] +head = "head_addons.js head_compat.js" + +["test_update_ignorecompat.js"] +skip-if = ["true"] # Bug 676922 Bug 1437697 + +["test_update_isPrivileged.js"] +skip-if = ["condprof"] # Bug 1769184 - by design for now + +["test_update_noSystemAddonUpdate.js"] +head = "head_addons.js head_system_addons.js" + +["test_update_strictcompat.js"] +head = "head_addons.js head_compat.js" + +["test_update_theme.js"] + +["test_update_webextensions.js"] +tags = "webextensions" + +["test_updatecheck.js"] + +["test_updatecheck_errors.js"] + +["test_updatecheck_json.js"] + +["test_updateid.js"] + +["test_updateversion.js"] + +["test_upgrade.js"] +head = "head_addons.js head_compat.js" +run-sequentially = "Uses global XCurProcD dir." + +["test_upgrade_incompatible.js"] + +["test_webextension.js"] +tags = "webextensions" + +["test_webextension_events.js"] +tags = "webextensions" + +["test_webextension_icons.js"] +tags = "webextensions" + +["test_webextension_install.js"] +tags = "webextensions" + +["test_webextension_install_syntax_error.js"] +tags = "webextensions" + +["test_webextension_langpack.js"] +tags = "webextensions" + +["test_webextension_theme.js"] +tags = "webextensions" diff --git a/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi b/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi Binary files differnew file mode 100644 index 0000000000..f2948e6994 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi diff --git a/toolkit/mozapps/extensions/test/xpinstall/authRedirect.sjs b/toolkit/mozapps/extensions/test/xpinstall/authRedirect.sjs new file mode 100644 index 0000000000..fffcb9f255 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/authRedirect.sjs @@ -0,0 +1,21 @@ +// Simple script redirects to the query part of the uri if the browser +// authenticates with username "testuser" password "testpass" + +function handleRequest(request, response) { + if (request.hasHeader("Authorization")) { + if ( + request.getHeader("Authorization") == "Basic dGVzdHVzZXI6dGVzdHBhc3M=" + ) { + response.setStatusLine(request.httpVersion, 302, "Found"); + response.setHeader("Location", request.queryString); + response.write("See " + request.queryString); + } else { + response.setStatusLine(request.httpVersion, 403, "Forbidden"); + response.write("Invalid credentials"); + } + } else { + response.setStatusLine(request.httpVersion, 401, "Authentication required"); + response.setHeader("WWW-Authenticate", 'basic realm="XPInstall"', false); + response.write("Unauthenticated request"); + } +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser.toml b/toolkit/mozapps/extensions/test/xpinstall/browser.toml new file mode 100644 index 0000000000..f6ca43982e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser.toml @@ -0,0 +1,175 @@ +[DEFAULT] +support-files = [ + "amosigned.xpi", + "authRedirect.sjs", + "bug540558.html", + "bug638292.html", + "bug645699.html", + "cookieRedirect.sjs", + "corrupt.xpi", + "empty.xpi", + "enabled.html", + "hashRedirect.sjs", + "head.js", + "incompatible.xpi", + "installchrome.html", + "installtrigger.html", + "installtrigger_frame.html", + "navigate.html", + "recommended.xpi", + "redirect.sjs", + "slowinstall.sjs", + "startsoftwareupdate.html", + "triggerredirect.html", + "unsigned.xpi", + "unsigned_mv3.xpi", + "webmidi_permission.xpi", + "../xpcshell/data/signing_checks/privileged.xpi", +] + +["browser_amosigned_trigger.js"] +https_first_disabled = true # Bug 1737265 + +["browser_amosigned_trigger_iframe.js"] +https_first_disabled = true # Bug 1737265 + +["browser_amosigned_url.js"] +https_first_disabled = true # Bug 1737265 + +["browser_auth.js"] +https_first_disabled = true # Bug 1737265 + +["browser_auth2.js"] + +["browser_auth3.js"] + +["browser_auth4.js"] +https_first_disabled = true # Bug 1737265 + +["browser_badargs.js"] +https_first_disabled = true # Bug 1737265 + +["browser_badargs2.js"] +https_first_disabled = true # Bug 1737265 + +["browser_badhash.js"] + +["browser_badhashtype.js"] + +["browser_block_fullscreen_prompt.js"] +https_first_disabled = true # Bug 1737265 +skip-if = ["os == 'mac' && debug"] #Bug 1590136 + +["browser_bug540558.js"] +https_first_disabled = true # Bug 1737265 + +["browser_bug611242.js"] + +["browser_bug638292.js"] + +["browser_bug645699.js"] + +["browser_bug645699_postDownload.js"] + +["browser_bug672485.js"] +skip-if = ["true"] # disabled due to a leak. See bug 682410. + +["browser_containers.js"] +https_first_disabled = true # Bug 1737265 + +["browser_cookies.js"] + +["browser_cookies2.js"] +https_first_disabled = true # Bug 1737265 + +["browser_cookies3.js"] +https_first_disabled = true # Bug 1737265 + +["browser_cookies4.js"] +skip-if = ["true"] # Bug 1084646 + +["browser_corrupt.js"] +https_first_disabled = true # Bug 1737265 + +["browser_datauri.js"] + +["browser_doorhanger_installs.js"] +https_first_disabled = true # Bug 1737265 +skip-if = [ + "os == 'win' && os_version == '10.0' && bits == 64", #Bug 1615449 +] + +["browser_empty.js"] + +["browser_enabled.js"] + +["browser_hash.js"] +https_first_disabled = true # Bug 1737265 + +["browser_hash2.js"] +https_first_disabled = true # Bug 1737265 + +["browser_httphash.js"] +https_first_disabled = true # Bug 1737265 + +["browser_httphash2.js"] + +["browser_httphash3.js"] +https_first_disabled = true # Bug 1737265 + +["browser_httphash4.js"] +https_first_disabled = true # Bug 1737265 + +["browser_httphash5.js"] +https_first_disabled = true # Bug 1737265 + +["browser_httphash6.js"] +skip-if = ["true"] # Bug 1449788 + +["browser_installchrome.js"] +https_first_disabled = true # Bug 1737265 + +["browser_localfile.js"] + +["browser_localfile2.js"] + +["browser_localfile3.js"] + +["browser_localfile4.js"] + +["browser_localfile4_postDownload.js"] + +["browser_newwindow.js"] +skip-if = ["!debug"] # This is a test for leaks, see comment in the test. + +["browser_offline.js"] + +["browser_privatebrowsing.js"] +https_first_disabled = true # Bug 1737265 +skip-if = ["debug"] # Bug 1541577 - leaks on debug + +["browser_relative.js"] +https_first_disabled = true # Bug 1737265 + +["browser_required_useractivation.js"] + +["browser_softwareupdate.js"] +https_first_disabled = true # Bug 1737265 + +["browser_trigger_redirect.js"] +https_first_disabled = true # Bug 1737265 + +["browser_unsigned_trigger.js"] +https_first_disabled = true # Bug 1737265 +skip-if = ["require_signing"] + +["browser_unsigned_trigger_iframe.js"] +https_first_disabled = true # Bug 1737265 +skip-if = ["require_signing"] + +["browser_unsigned_trigger_xorigin.js"] +https_first_disabled = true # Bug 1737265 + +["browser_unsigned_url.js"] +https_first_disabled = true # Bug 1737265 +skip-if = ["require_signing"] diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger.js b/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger.js new file mode 100644 index 0000000000..97aa9e1a94 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +// ---------------------------------------------------------------------------- +// Tests installing an unsigned add-on through an InstallTrigger call in web +// content. +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installConfirmCallback = confirm_install; + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.finalContentEvent = "InstallComplete"; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: TESTROOT + "amosigned.xpi", + IconURL: TESTROOT + "icon.png", + toString() { + return this.URL; + }, + }, + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function confirm_install(panel) { + is(panel.getAttribute("name"), "XPI Test", "Should have seen the name"); + return true; +} + +function install_ended(install, addon) { + AddonTestUtils.checkInstallInfo(install, { + method: "installTrigger", + source: "test-host", + sourceURL: /http:\/\/example.com\/.*\/installtrigger.html/, + }); + + return addon.uninstall(); +} + +const finish_test = async function (count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + PermissionTestUtils.remove("http://example.com", "install"); + + const results = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + return { + return: content.document.getElementById("return").textContent, + status: content.document.getElementById("status").textContent, + }; + } + ); + + is(results.return, "true", "installTrigger should have claimed success"); + is(results.status, "0", "Callback should have seen a success"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +}; diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger_iframe.js b/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger_iframe.js new file mode 100644 index 0000000000..f9d91e1cbd --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger_iframe.js @@ -0,0 +1,77 @@ +// ---------------------------------------------------------------------------- +// Test for bug 589598 - Ensure that installing through InstallTrigger +// works in an iframe in web content. + +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installConfirmCallback = confirm_install; + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.finalContentEvent = "InstallComplete"; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var inner_url = encodeURIComponent( + TESTROOT + + "installtrigger.html?" + + encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: TESTROOT + "amosigned.xpi", + IconURL: TESTROOT + "icon.png", + toString() { + return this.URL; + }, + }, + }) + ) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger_frame.html?" + inner_url + ); +} + +function confirm_install(panel) { + is(panel.getAttribute("name"), "XPI Test", "Should have seen the name"); + return true; +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +const finish_test = async function (count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + PermissionTestUtils.remove("http://example.com", "install"); + + const results = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + return { + return: content.frames[0].document.getElementById("return").textContent, + status: content.frames[0].document.getElementById("status").textContent, + }; + } + ); + + is( + results.return, + "true", + "installTrigger in iframe should have claimed success" + ); + is(results.status, "0", "Callback in iframe should have seen a success"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +}; diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_url.js b/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_url.js new file mode 100644 index 0000000000..a6b50c2b27 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_url.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +// ---------------------------------------------------------------------------- +// Tests installing an unsigned add-on by navigating directly to the url +function test() { + waitForExplicitFinish(); + SpecialPowers.pushPrefEnv( + { + set: [ + // Relax the user input requirements while running this test. + ["xpinstall.userActivation.required", false], + ], + }, + runTest + ); +} + +function runTest() { + Harness.installConfirmCallback = confirm_install; + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => { + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "amosigned.xpi" + ); + }); +} + +function confirm_install(panel) { + is(panel.getAttribute("name"), "XPI Test", "Should have seen the name"); + return true; +} + +function install_ended(install, addon) { + AddonTestUtils.checkInstallInfo(install, { + method: "link", + source: "unknown", + sourceURL: undefined, + }); + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} +// ---------------------------------------------------------------------------- diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_auth.js b/toolkit/mozapps/extensions/test/xpinstall/browser_auth.js new file mode 100644 index 0000000000..2248af4270 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_auth.js @@ -0,0 +1,68 @@ +// ---------------------------------------------------------------------------- +// Test whether an install succeeds when authentication is required +// This verifies bug 312473 +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + // Turn off the authentication dialog blocking for this test. + Services.prefs.setBoolPref( + "network.auth.non-web-content-triggered-resources-http-auth-allow", + true + ); + + Harness.authenticationCallback = get_auth_info; + Harness.downloadFailedCallback = download_failed; + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + Services.prefs.setIntPref("network.auth.subresource-http-auth-allow", 2); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": + TESTROOT + "authRedirect.sjs?" + TESTROOT + "amosigned.xpi", + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function get_auth_info() { + return ["testuser", "testpass"]; +} + +function download_failed(install) { + ok(false, "Install should not have failed"); +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + var authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].getService( + Ci.nsIHttpAuthManager + ); + authMgr.clearAll(); + + PermissionTestUtils.remove("http://example.com", "install"); + + Services.prefs.clearUserPref( + "network.auth.non-web-content-triggered-resources-http-auth-allow" + ); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_auth2.js b/toolkit/mozapps/extensions/test/xpinstall/browser_auth2.js new file mode 100644 index 0000000000..b5a5c09749 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_auth2.js @@ -0,0 +1,73 @@ +// ---------------------------------------------------------------------------- +// Test whether an install fails when authentication is required and bad +// credentials are given +// This verifies bug 312473 +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + // Turn off the authentication dialog blocking for this test. + Services.prefs.setBoolPref( + "network.auth.non-web-content-triggered-resources-http-auth-allow", + true + ); + + requestLongerTimeout(2); + Harness.authenticationCallback = get_auth_info; + Harness.downloadFailedCallback = download_failed; + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": + TESTROOT + "authRedirect.sjs?" + TESTROOT + "amosigned.xpi", + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function get_auth_info() { + return ["baduser", "badpass"]; +} + +function download_failed(install) { + is( + install.error, + AddonManager.ERROR_NETWORK_FAILURE, + "Install should have failed" + ); +} + +function install_ended(install, addon) { + ok(false, "Add-on should not have installed"); + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 0, "No add-ons should have been installed"); + var authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].getService( + Ci.nsIHttpAuthManager + ); + authMgr.clearAll(); + + PermissionTestUtils.remove("http://example.com", "install"); + + Services.prefs.clearUserPref( + "network.auth.non-web-content-triggered-resources-http-auth-allow" + ); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_auth3.js b/toolkit/mozapps/extensions/test/xpinstall/browser_auth3.js new file mode 100644 index 0000000000..d348be6d30 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_auth3.js @@ -0,0 +1,72 @@ +// ---------------------------------------------------------------------------- +// Test whether an install fails when authentication is required and it is +// canceled +// This verifies bug 312473 +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + // Turn off the authentication dialog blocking for this test. + Services.prefs.setBoolPref( + "network.auth.non-web-content-triggered-resources-http-auth-allow", + true + ); + + Harness.authenticationCallback = get_auth_info; + Harness.downloadFailedCallback = download_failed; + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": + TESTROOT + "authRedirect.sjs?" + TESTROOT + "amosigned.xpi", + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function get_auth_info() { + return null; +} + +function download_failed(install) { + is( + install.error, + AddonManager.ERROR_NETWORK_FAILURE, + "Install should have failed" + ); +} + +function install_ended(install, addon) { + ok(false, "Add-on should not have installed"); + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 0, "No add-ons should have been installed"); + var authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].getService( + Ci.nsIHttpAuthManager + ); + authMgr.clearAll(); + + PermissionTestUtils.remove("http://example.com", "install"); + + Services.prefs.clearUserPref( + "network.auth.non-web-content-triggered-resources-http-auth-allow" + ); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_auth4.js b/toolkit/mozapps/extensions/test/xpinstall/browser_auth4.js new file mode 100644 index 0000000000..46ee2b5cb6 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_auth4.js @@ -0,0 +1,71 @@ +// Test whether a request for auth for an XPI switches to the appropriate tab +var gNewTab; + +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + // Turn off the authentication dialog blocking for this test. + Services.prefs.setBoolPref( + "network.auth.non-web-content-triggered-resources-http-auth-allow", + true + ); + + Harness.authenticationCallback = get_auth_info; + Harness.downloadFailedCallback = download_failed; + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": + TESTROOT + "authRedirect.sjs?" + TESTROOT + "amosigned.xpi", + }) + ); + gNewTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser.getBrowserForTab(gNewTab), + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function get_auth_info() { + is( + gBrowser.selectedTab, + gNewTab, + "Should have focused the tab loading the XPI" + ); + return ["testuser", "testpass"]; +} + +function download_failed(install) { + ok(false, "Install should not have failed"); +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + var authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].getService( + Ci.nsIHttpAuthManager + ); + authMgr.clearAll(); + + PermissionTestUtils.remove("http://example.com", "install"); + + Services.prefs.clearUserPref( + "network.auth.non-web-content-triggered-resources-http-auth-allow" + ); + + gBrowser.removeTab(gNewTab); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_badargs.js b/toolkit/mozapps/extensions/test/xpinstall/browser_badargs.js new file mode 100644 index 0000000000..8f75a7a653 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_badargs.js @@ -0,0 +1,49 @@ +// ---------------------------------------------------------------------------- +// Test whether passing a simple string to InstallTrigger.install throws an +// exception +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + waitForExplicitFinish(); + + var triggers = encodeURIComponent(JSON.stringify(TESTROOT + "amosigned.xpi")); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, TESTROOT); + + ContentTask.spawn(gBrowser.selectedBrowser, null, function () { + return new Promise(resolve => { + addEventListener( + "load", + () => { + content.addEventListener("InstallTriggered", () => { + resolve(content.document.getElementById("return").textContent); + }); + }, + true + ); + }); + }).then(page_loaded); + + // In non-e10s the exception in the content page would trigger a test failure + if (!gMultiProcessBrowser) { + expectUncaughtException(); + } + + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function page_loaded(result) { + is(result, "exception", "installTrigger should have failed"); + + // In non-e10s the exception from the page is thrown after the event so we + // have to spin the event loop to make sure it arrives so expectUncaughtException + // sees it. + executeSoon(() => { + gBrowser.removeCurrentTab(); + finish(); + }); +} +// ---------------------------------------------------------------------------- diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_badargs2.js b/toolkit/mozapps/extensions/test/xpinstall/browser_badargs2.js new file mode 100644 index 0000000000..8376cc83a4 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_badargs2.js @@ -0,0 +1,55 @@ +// ---------------------------------------------------------------------------- +// Test whether passing an undefined url InstallTrigger.install throws an +// exception +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + waitForExplicitFinish(); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: undefined, + }, + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, TESTROOT); + + ContentTask.spawn(gBrowser.selectedBrowser, null, function () { + return new Promise(resolve => { + addEventListener( + "load", + () => { + content.addEventListener("InstallTriggered", () => { + resolve(content.document.getElementById("return").textContent); + }); + }, + true + ); + }); + }).then(page_loaded); + + // In non-e10s the exception in the content page would trigger a test failure + if (!gMultiProcessBrowser) { + expectUncaughtException(); + } + + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function page_loaded(result) { + is(result, "exception", "installTrigger should have failed"); + + // In non-e10s the exception from the page is thrown after the event so we + // have to spin the event loop to make sure it arrives so expectUncaughtException + // sees it. + executeSoon(() => { + gBrowser.removeCurrentTab(); + finish(); + }); +} +// ---------------------------------------------------------------------------- diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_badhash.js b/toolkit/mozapps/extensions/test/xpinstall/browser_badhash.js new file mode 100644 index 0000000000..985eae9cfc --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_badhash.js @@ -0,0 +1,46 @@ +// ---------------------------------------------------------------------------- +// Test whether an install fails when an invalid hash is included +// This verifies bug 302284 +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.downloadFailedCallback = download_failed; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: TESTROOT + "amosigned.xpi", + Hash: "sha1:643b08418599ddbd1ea8a511c90696578fb844b9", + toString() { + return this.URL; + }, + }, + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function download_failed(install) { + is(install.error, AddonManager.ERROR_INCORRECT_HASH, "Install should fail"); +} + +function finish_test(count) { + is(count, 0, "No add-ons should have been installed"); + PermissionTestUtils.remove("http://example.com/", "install"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_badhashtype.js b/toolkit/mozapps/extensions/test/xpinstall/browser_badhashtype.js new file mode 100644 index 0000000000..f6c1b17d1f --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_badhashtype.js @@ -0,0 +1,46 @@ +// ---------------------------------------------------------------------------- +// Test whether an install fails when an unknown hash type is included +// This verifies bug 302284 +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.downloadFailedCallback = download_failed; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: TESTROOT + "amosigned.xpi", + Hash: "foo:3d0dc22e1f394e159b08aaf5f0f97de4d5c65f4f", + toString() { + return this.URL; + }, + }, + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function download_failed(install) { + is(install.error, AddonManager.ERROR_INCORRECT_HASH, "Install should fail"); +} + +function finish_test(count) { + is(count, 0, "No add-ons should have been installed"); + PermissionTestUtils.remove("http://example.com/", "install"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_block_fullscreen_prompt.js b/toolkit/mozapps/extensions/test/xpinstall/browser_block_fullscreen_prompt.js new file mode 100644 index 0000000000..97f09d9a9c --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_block_fullscreen_prompt.js @@ -0,0 +1,129 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This test tends to trigger a race in the fullscreen time telemetry, +// where the fullscreen enter and fullscreen exit events (which use the +// same histogram ID) overlap. That causes TelemetryStopwatch to log an +// error. +SimpleTest.ignoreAllUncaughtExceptions(true); + +/** + * Spawns content task in browser to enter / leave fullscreen + * @param browser - Browser to use for JS fullscreen requests + * @param {Boolean} fullscreenState - true to enter fullscreen, false to leave + */ +function changeFullscreen(browser, fullscreenState) { + return SpecialPowers.spawn( + browser, + [fullscreenState], + async function (state) { + if (state) { + await content.document.body.requestFullscreen(); + } else { + await content.document.exitFullscreen(); + } + } + ); +} + +function triggerInstall(browser, xpi_url) { + return SpecialPowers.spawn(browser, [xpi_url], async function (xpi_url) { + content.location = xpi_url; + }); +} + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + // Relax the user input requirements while running this test. + set: [["xpinstall.userActivation.required", false]], + }); +}); + +// This tests if addon installation is blocked when requested in fullscreen +add_task(async function testFullscreenBlockAddonInstallPrompt() { + // Open example.com + await BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT); + + // Enter and wait for fullscreen + await changeFullscreen(gBrowser.selectedBrowser, true); + await TestUtils.waitForCondition( + () => window.fullScreen, + "Waiting for window to enter fullscreen" + ); + + // Trigger addon installation and expect it to be blocked + let addonEventPromise = TestUtils.topicObserved( + "addon-install-fullscreen-blocked" + ); + await triggerInstall(gBrowser.selectedBrowser, "amosigned.xpi"); + await addonEventPromise; + + // Test if addon installation prompt has been blocked + let panelOpened; + try { + panelOpened = await TestUtils.waitForCondition( + () => PopupNotifications.isPanelOpen, + 100, + 10 + ); + } catch (ex) { + panelOpened = false; + } + is(panelOpened, false, "Addon installation prompt not opened"); + + window.fullScreen = false; + await BrowserTestUtils.waitForEvent(window, "fullscreenchange"); + await BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// This tests if the addon install prompt is closed when entering fullscreen +add_task(async function testFullscreenCloseAddonInstallPrompt() { + // Open example.com + await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com"); + + // Trigger addon installation + let addonEventPromise = TestUtils.topicObserved( + "webextension-permission-prompt" + ); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [TESTROOT + "amosigned.xpi"], + xpi_url => { + this.content.location = xpi_url; + } + ); + // Wait for addon install event + info("Wait for webextension-permission-prompt"); + await addonEventPromise; + + // Test if addon installation prompt is visible + await TestUtils.waitForCondition( + () => PopupNotifications.isPanelOpen, + "Waiting for addon installation prompt to open" + ); + Assert.ok( + PopupNotifications.getNotification( + "addon-webext-permissions", + gBrowser.selectedBrowser + ) != null, + "Opened notification is webextension permissions prompt" + ); + + // Switch to fullscreen and test for addon installation prompt close + await changeFullscreen(gBrowser.selectedBrowser, true); + await TestUtils.waitForCondition( + () => window.fullScreen, + "Waiting for window to enter fullscreen" + ); + await TestUtils.waitForCondition( + () => !PopupNotifications.isPanelOpen, + "Waiting for addon installation prompt to close" + ); + + window.fullScreen = false; + await BrowserTestUtils.waitForEvent(window, "fullscreenchange"); + await BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_bug540558.js b/toolkit/mozapps/extensions/test/xpinstall/browser_bug540558.js new file mode 100644 index 0000000000..ece34583a1 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug540558.js @@ -0,0 +1,31 @@ +// ---------------------------------------------------------------------------- +// Tests that calling InstallTrigger.installChrome works +function test() { + // This test depends on InstallTrigger.installChrome availability. + setInstallTriggerPrefs(); + + Harness.installEndedCallback = check_xpi_install; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString(gBrowser, TESTROOT + "bug540558.html"); +} + +function check_xpi_install(install, addon) { + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + PermissionTestUtils.remove("http://example.com", "install"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_bug611242.js b/toolkit/mozapps/extensions/test/xpinstall/browser_bug611242.js new file mode 100644 index 0000000000..98cf433f6f --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug611242.js @@ -0,0 +1,34 @@ +// ---------------------------------------------------------------------------- +// Test whether setting a new property in InstallTrigger then persists to other +// page loads +add_task(async function test() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.InstallTrigger.enabled", true], + ["extensions.InstallTriggerImpl.enabled", true], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: TESTROOT + "enabled.html" }, + async function (browser) { + await SpecialPowers.spawn(browser, [], () => { + content.wrappedJSObject.InstallTrigger.enabled.k = function () {}; + }); + + BrowserTestUtils.startLoadingURIString( + browser, + TESTROOT2 + "enabled.html" + ); + await BrowserTestUtils.browserLoaded(browser); + await SpecialPowers.spawn(browser, [], () => { + is( + content.wrappedJSObject.InstallTrigger.enabled.k, + undefined, + "Property should not be defined" + ); + }); + } + ); +}); +// ---------------------------------------------------------------------------- diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_bug638292.js b/toolkit/mozapps/extensions/test/xpinstall/browser_bug638292.js new file mode 100644 index 0000000000..078d94cb50 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug638292.js @@ -0,0 +1,51 @@ +// ---------------------------------------------------------------------------- +// Test whether an InstallTrigger.enabled is working +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.InstallTrigger.enabled", true], + ["extensions.InstallTriggerImpl.enabled", true], + ], + }); + + let testtab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "bug638292.html" + ); + + async function verify(link, button) { + info("Clicking " + link); + + let loadedPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "#" + link, + { button }, + gBrowser.selectedBrowser + ); + + let newtab = await loadedPromise; + + let result = await SpecialPowers.spawn( + newtab.linkedBrowser, + [], + async function () { + return content.document.getElementById("enabled").textContent == "true"; + } + ); + + ok(result, "installTrigger for " + link + " should have been enabled"); + + // Focus the old tab (link3 is opened in the background) + if (link != "link3") { + await BrowserTestUtils.switchTab(gBrowser, testtab); + } + gBrowser.removeTab(newtab); + } + + await verify("link1", 0); + await verify("link2", 0); + await verify("link3", 1); + + gBrowser.removeCurrentTab(); +}); diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js b/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js new file mode 100644 index 0000000000..690ac2b3eb --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js @@ -0,0 +1,69 @@ +// ---------------------------------------------------------------------------- +// Tests installing an unsigned add-on through an InstallTrigger call in web +// content. This should be blocked by the whitelist check. +// This verifies bug 645699 +function test() { + if ( + !SpecialPowers.Services.prefs.getBoolPref( + "extensions.InstallTrigger.enabled" + ) || + !SpecialPowers.Services.prefs.getBoolPref( + "extensions.InstallTriggerImpl.enabled" + ) + ) { + ok(true, "InstallTrigger is not enabled"); + return; + } + + // prompt prior to download + SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.postDownloadThirdPartyPrompt", false], + ["extensions.InstallTrigger.requireUserInput", false], + // Relax the user input requirements while running this test. + ["xpinstall.userActivation.required", false], + ], + }); + + Harness.installConfirmCallback = confirm_install; + Harness.installBlockedCallback = allow_blocked; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.org/", + "install", + Services.perms.ALLOW_ACTION + ); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString(gBrowser, TESTROOT + "bug645699.html"); +} + +function allow_blocked(installInfo) { + is( + installInfo.browser, + gBrowser.selectedBrowser, + "Install should have been triggered by the right browser" + ); + is( + installInfo.originatingURI.spec, + gBrowser.currentURI.spec, + "Install should have been triggered by the right uri" + ); + return false; +} + +function confirm_install(panel) { + ok(false, "Should not see the install dialog"); + return false; +} + +function finish_test(count) { + is(count, 0, "0 Add-ons should have been successfully installed"); + PermissionTestUtils.remove("http://example.org/", "install"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} +// ---------------------------------------------------------------------------- diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699_postDownload.js b/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699_postDownload.js new file mode 100644 index 0000000000..aa8b948c14 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699_postDownload.js @@ -0,0 +1,55 @@ +// ---------------------------------------------------------------------------- +// Tests installing an unsigned add-on through an InstallTrigger call in web +// content. This should be blocked by the origin allow check. +// This verifies bug 645699 +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installConfirmCallback = confirm_install; + Harness.installBlockedCallback = allow_blocked; + Harness.installsCompletedCallback = finish_test; + // Prevent the Harness from ending the test on download cancel. + Harness.downloadCancelledCallback = () => { + return false; + }; + + Harness.setup(); + + PermissionTestUtils.add( + "http://example.org/", + "install", + Services.perms.ALLOW_ACTION + ); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString(gBrowser, TESTROOT + "bug645699.html"); +} + +function allow_blocked(installInfo) { + is( + installInfo.browser, + gBrowser.selectedBrowser, + "Install should have been triggered by the right browser" + ); + is( + installInfo.originatingURI.spec, + gBrowser.currentURI.spec, + "Install should have been triggered by the right uri" + ); + return false; +} + +function confirm_install(panel) { + ok(false, "Should not see the install dialog"); + return false; +} + +function finish_test(count) { + is(count, 0, "0 Add-ons should have been successfully installed"); + PermissionTestUtils.remove("http://example.org/", "install"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} +// ---------------------------------------------------------------------------- diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_bug672485.js b/toolkit/mozapps/extensions/test/xpinstall/browser_bug672485.js new file mode 100644 index 0000000000..216d543458 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug672485.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var gWindowWatcher = null; + +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installConfirmCallback = confirm_install; + Harness.installCancelledCallback = cancelled_install; + Harness.installEndedCallback = complete_install; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + gWindowWatcher = Services.ww; + delete Services.ww; + is(Services.ww, undefined, "Services.ww should now be undefined"); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": TESTROOT + "amosigned.xpi", + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function confirm_install(panel) { + ok(false, "Should not see the install dialog"); + return false; +} + +function cancelled_install() { + ok(true, "Install should b cancelled"); +} + +function complete_install() { + ok(false, "Install should not have completed"); + return false; +} + +function finish_test(count) { + is(count, 0, "0 Add-ons should have been successfully installed"); + + gBrowser.removeCurrentTab(); + + Services.ww = gWindowWatcher; + + PermissionTestUtils.remove("http://example.com", "install"); + + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_containers.js b/toolkit/mozapps/extensions/test/xpinstall/browser_containers.js new file mode 100644 index 0000000000..486f391c9e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_containers.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +const MY_CONTEXT = 2; +let gDidSeeChannel = false; + +function check_channel(subject) { + if (!(subject instanceof Ci.nsIHttpChannel)) { + return; + } + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + let uri = channel.URI; + if (!uri || !uri.spec.endsWith("amosigned.xpi")) { + return; + } + gDidSeeChannel = true; + ok(true, "Got request for " + uri.spec); + + let loadInfo = channel.loadInfo; + is( + loadInfo.originAttributes.userContextId, + MY_CONTEXT, + "Got expected usercontextid" + ); +} +// ---------------------------------------------------------------------------- +// Tests we send the right cookies when installing through an InstallTrigger call +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installConfirmCallback = confirm_install; + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.finalContentEvent = "InstallComplete"; + Harness.setup(); + + let principal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI("http://example.com/"), + { userContextId: MY_CONTEXT } + ); + + PermissionTestUtils.add(principal, "install", Services.perms.ALLOW_ACTION); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: TESTROOT + "amosigned.xpi", + IconURL: TESTROOT + "icon.png", + toString() { + return this.URL; + }, + }, + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "", { + userContextId: MY_CONTEXT, + }); + Services.obs.addObserver(check_channel, "http-on-before-connect"); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function confirm_install(panel) { + is(panel.getAttribute("name"), "XPI Test", "Should have seen the name"); + return true; +} + +function install_ended(install, addon) { + AddonTestUtils.checkInstallInfo(install, { + method: "installTrigger", + source: "test-host", + sourceURL: /http:\/\/example.com\/.*\/installtrigger.html/, + }); + return addon.uninstall(); +} + +const finish_test = async function (count) { + ok( + gDidSeeChannel, + "Should have seen the request for the XPI and verified it was sent the right way." + ); + is(count, 1, "1 Add-on should have been successfully installed"); + + Services.obs.removeObserver(check_channel, "http-on-before-connect"); + + PermissionTestUtils.remove("http://example.com", "install"); + + const results = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + return { + return: content.document.getElementById("return").textContent, + status: content.document.getElementById("status").textContent, + }; + } + ); + + is(results.return, "true", "installTrigger should have claimed success"); + is(results.status, "0", "Callback should have seen a success"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +}; diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_cookies.js b/toolkit/mozapps/extensions/test/xpinstall/browser_cookies.js new file mode 100644 index 0000000000..fc08de89b1 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_cookies.js @@ -0,0 +1,42 @@ +// ---------------------------------------------------------------------------- +// Test that an install that requires cookies to be sent fails when no cookies +// are set +// This verifies bug 462739 +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.downloadFailedCallback = download_failed; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Cookie check": + TESTROOT + "cookieRedirect.sjs?" + TESTROOT + "amosigned.xpi", + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function download_failed(install) { + is(install.error, AddonManager.ERROR_NETWORK_FAILURE, "Install should fail"); +} + +function finish_test(count) { + is(count, 0, "No add-ons should have been installed"); + PermissionTestUtils.remove("http://example.com", "install"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_cookies2.js b/toolkit/mozapps/extensions/test/xpinstall/browser_cookies2.js new file mode 100644 index 0000000000..1465f0f93d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_cookies2.js @@ -0,0 +1,64 @@ +// ---------------------------------------------------------------------------- +// Test that an install that requires cookies to be sent succeeds when cookies +// are set +// This verifies bug 462739 +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + Services.cookies.add( + "example.com", + "/browser/" + RELATIVE_DIR, + "xpinstall", + "true", + false, + false, + true, + Date.now() / 1000 + 60, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTP + ); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Cookie check": + TESTROOT + "cookieRedirect.sjs?" + TESTROOT + "amosigned.xpi", + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + Services.cookies.remove( + "example.com", + "xpinstall", + "/browser/" + RELATIVE_DIR, + {} + ); + + PermissionTestUtils.remove("http://example.com", "install"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_cookies3.js b/toolkit/mozapps/extensions/test/xpinstall/browser_cookies3.js new file mode 100644 index 0000000000..03ceead636 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_cookies3.js @@ -0,0 +1,68 @@ +// ---------------------------------------------------------------------------- +// Test that an install that requires cookies to be sent succeeds when cookies +// are set and third party cookies are disabled. +// This verifies bug 462739 +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + Services.cookies.add( + "example.com", + "/browser/" + RELATIVE_DIR, + "xpinstall", + "true", + false, + false, + true, + Date.now() / 1000 + 60, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTP + ); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + Services.prefs.setIntPref("network.cookie.cookieBehavior", 1); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Cookie check": + TESTROOT + "cookieRedirect.sjs?" + TESTROOT + "amosigned.xpi", + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + Services.cookies.remove( + "example.com", + "xpinstall", + "/browser/" + RELATIVE_DIR, + {} + ); + + Services.prefs.clearUserPref("network.cookie.cookieBehavior"); + + PermissionTestUtils.remove("http://example.com", "install"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_cookies4.js b/toolkit/mozapps/extensions/test/xpinstall/browser_cookies4.js new file mode 100644 index 0000000000..931a9a5ff5 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_cookies4.js @@ -0,0 +1,68 @@ +// ---------------------------------------------------------------------------- +// Test that an install that requires cookies to be sent fails when cookies +// are set and third party cookies are disabled and the request is to a third +// party. +// This verifies bug 462739 +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.downloadFailedCallback = download_failed; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + Services.cookies.add( + "example.org", + "/browser/" + RELATIVE_DIR, + "xpinstall", + "true", + false, + false, + true, + Date.now() / 1000 + 60, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTP + ); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + Services.prefs.setIntPref("network.cookie.cookieBehavior", 1); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Cookie check": + TESTROOT2 + "cookieRedirect.sjs?" + TESTROOT + "amosigned.xpi", + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function download_failed(install) { + is(install.error, AddonManager.ERROR_NETWORK_FAILURE, "Install should fail"); +} + +function finish_test(count) { + is(count, 0, "No add-ons should have been installed"); + + Services.cookies.remove( + "example.org", + "xpinstall", + "/browser/" + RELATIVE_DIR, + {} + ); + + Services.prefs.clearUserPref("network.cookie.cookieBehavior"); + PermissionTestUtils.remove("http://example.com", "install"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_corrupt.js b/toolkit/mozapps/extensions/test/xpinstall/browser_corrupt.js new file mode 100644 index 0000000000..11ecc6446d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_corrupt.js @@ -0,0 +1,53 @@ +// ---------------------------------------------------------------------------- +// Test whether an install fails when the xpi is corrupt. +function test() { + // This test currently depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.downloadFailedCallback = download_failed; + Harness.installsCompletedCallback = finish_test; + Harness.finalContentEvent = "InstallComplete"; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Corrupt XPI": TESTROOT + "corrupt.xpi", + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function download_failed(install) { + is(install.error, AddonManager.ERROR_CORRUPT_FILE, "Install should fail"); +} + +const finish_test = async function (count) { + is(count, 0, "No add-ons should have been installed"); + PermissionTestUtils.remove("http://example.com", "install"); + + const results = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + return { + return: content.document.getElementById("return").textContent, + status: content.document.getElementById("status").textContent, + }; + } + ); + + is(results.status, "-207", "Callback should have seen the failure"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +}; diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_datauri.js b/toolkit/mozapps/extensions/test/xpinstall/browser_datauri.js new file mode 100644 index 0000000000..08ec7e41cb --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_datauri.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +// ---------------------------------------------------------------------------- +// Checks that a chained redirect through a data URI and javascript is blocked + +function setup_redirect(aSettings) { + var url = TESTROOT + "redirect.sjs?mode=setup"; + for (var name in aSettings) { + url += "&" + name + "=" + encodeURIComponent(aSettings[name]); + } + + var req = new XMLHttpRequest(); + req.open("GET", url, false); + req.send(null); +} + +function test() { + waitForExplicitFinish(); + SpecialPowers.pushPrefEnv( + { + set: [ + ["network.allow_redirect_to_data", true], + ["security.data_uri.block_toplevel_data_uri_navigations", false], + // Relax the user input requirements while running this test. + ["xpinstall.userActivation.required", false], + ], + }, + runTest + ); +} + +function runTest() { + Harness.installOriginBlockedCallback = install_blocked; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + setup_redirect({ + Location: + "data:text/html,<script>window.location.href='" + + TESTROOT + + "amosigned.xpi'</script>", + }); + + BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "redirect.sjs?mode=redirect" + ); +} + +function install_blocked(installInfo) { + is( + installInfo.installs.length, + 1, + "Got one AddonInstall instance as expected" + ); + AddonTestUtils.checkInstallInfo(installInfo.installs[0], { + method: "link", + source: "unknown", + sourceURL: /moz-nullprincipal:\{.*\}/, + }); +} + +function finish_test(count) { + is(count, 0, "No add-ons should have been installed"); + PermissionTestUtils.remove("http://example.com", "install"); + + gBrowser.removeCurrentTab(); + Harness.finish(); + finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js b/toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js new file mode 100644 index 0000000000..01c8089180 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js @@ -0,0 +1,1545 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// TODO(Bug 1789718): adapt to synthetic addon type implemented by the SitePermAddonProvider +// or remove if redundant, after the deprecated XPIProvider-based implementation is also removed. + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { ExtensionPermissions } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionPermissions.sys.mjs" +); +const { Management } = ChromeUtils.importESModule( + "resource://gre/modules/Extension.sys.mjs" +); + +const SECUREROOT = + "https://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/"; +const PROGRESS_NOTIFICATION = "addon-progress"; + +const CHROMEROOT = extractChromeRoot(gTestPath); + +AddonTestUtils.initMochitest(this); + +function waitForTick() { + return new Promise(resolve => executeSoon(resolve)); +} + +function getObserverTopic(aNotificationId) { + let topic = aNotificationId; + if (topic == "xpinstall-disabled") { + topic = "addon-install-disabled"; + } else if (topic == "addon-progress") { + topic = "addon-install-started"; + } else if (topic == "addon-installed") { + topic = "webextension-install-notify"; + } + return topic; +} + +async function waitForProgressNotification( + aPanelOpen = false, + aExpectedCount = 1, + wantDisabled = true, + expectedAnchorID = "unified-extensions-button", + win = window +) { + let notificationId = PROGRESS_NOTIFICATION; + info("Waiting for " + notificationId + " notification"); + + let topic = getObserverTopic(notificationId); + + let observerPromise = new Promise(resolve => { + Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + // Ignore the progress notification unless that is the notification we want + if ( + notificationId != PROGRESS_NOTIFICATION && + aTopic == getObserverTopic(PROGRESS_NOTIFICATION) + ) { + return; + } + Services.obs.removeObserver(observer, topic); + resolve(); + }, topic); + }); + + let panelEventPromise; + if (aPanelOpen) { + panelEventPromise = Promise.resolve(); + } else { + panelEventPromise = new Promise(resolve => { + win.PopupNotifications.panel.addEventListener( + "popupshowing", + function () { + resolve(); + }, + { once: true } + ); + }); + } + + await observerPromise; + await panelEventPromise; + await waitForTick(); + + info("Saw a notification"); + ok(win.PopupNotifications.isPanelOpen, "Panel should be open"); + is( + win.PopupNotifications.panel.childNodes.length, + aExpectedCount, + "Should be the right number of notifications" + ); + if (win.PopupNotifications.panel.childNodes.length) { + let nodes = Array.from(win.PopupNotifications.panel.childNodes); + let notification = nodes.find( + n => n.id == notificationId + "-notification" + ); + ok(notification, `Should have seen the right notification`); + is( + notification.button.hasAttribute("disabled"), + wantDisabled, + "The install button should be disabled?" + ); + + let n = win.PopupNotifications.getNotification(PROGRESS_NOTIFICATION); + is( + n?.anchorElement?.id || n?.anchorElement?.parentElement?.id, + expectedAnchorID, + "expected the right anchor ID" + ); + } + + return win.PopupNotifications.panel; +} + +function acceptAppMenuNotificationWhenShown( + id, + extensionId, + { + dismiss = false, + checkIncognito = false, + incognitoChecked = false, + incognitoHidden = false, + global = window, + } = {} +) { + const { AppMenuNotifications, PanelUI, document } = global; + return new Promise(resolve => { + let permissionChangePromise = null; + function appMenuPopupHidden() { + PanelUI.panel.removeEventListener("popuphidden", appMenuPopupHidden); + ok( + !PanelUI.menuButton.hasAttribute("badge-status"), + "badge is not set after addon-installed" + ); + resolve(permissionChangePromise); + } + function appMenuPopupShown() { + PanelUI.panel.removeEventListener("popupshown", appMenuPopupShown); + PanelUI.menuButton.click(); + } + function popupshown() { + let notification = AppMenuNotifications.activeNotification; + if (!notification) { + return; + } + + is(notification.id, id, `${id} notification shown`); + ok(PanelUI.isNotificationPanelOpen, "notification panel open"); + + PanelUI.notificationPanel.removeEventListener("popupshown", popupshown); + + let checkbox = document.getElementById("addon-incognito-checkbox"); + is(checkbox.hidden, incognitoHidden, "checkbox visibility is correct"); + is(checkbox.checked, incognitoChecked, "checkbox is marked as expected"); + + // If we're unchecking or checking the incognito property, this will + // trigger an update in ExtensionPermission, let's wait for it before + // returning from this promise. + if (incognitoChecked != checkIncognito) { + permissionChangePromise = new Promise(resolve => { + const listener = (type, change) => { + if (extensionId == change.extensionId) { + // Let's make sure we received the right message + let { permissions } = checkIncognito + ? change.added + : change.removed; + ok(permissions.includes("internal:privateBrowsingAllowed")); + resolve(); + } + }; + Management.once("change-permissions", listener); + }); + } + + checkbox.checked = checkIncognito; + + if (dismiss) { + // Dismiss the panel by clicking on the appMenu button. + PanelUI.panel.addEventListener("popupshown", appMenuPopupShown); + PanelUI.panel.addEventListener("popuphidden", appMenuPopupHidden); + PanelUI.menuButton.click(); + return; + } + + // Dismiss the panel by clicking the primary button. + let popupnotificationID = PanelUI._getPopupId(notification); + let popupnotification = document.getElementById(popupnotificationID); + + popupnotification.button.click(); + resolve(permissionChangePromise); + } + PanelUI.notificationPanel.addEventListener("popupshown", popupshown); + }); +} + +async function waitForNotification( + aId, + aExpectedCount = 1, + expectedAnchorID = "unified-extensions-button", + win = window +) { + info("Waiting for " + aId + " notification"); + + let topic = getObserverTopic(aId); + + let observerPromise; + if (aId !== "addon-webext-permissions") { + observerPromise = new Promise(resolve => { + Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + // Ignore the progress notification unless that is the notification we want + if ( + aId != PROGRESS_NOTIFICATION && + aTopic == getObserverTopic(PROGRESS_NOTIFICATION) + ) { + return; + } + Services.obs.removeObserver(observer, topic); + resolve(); + }, topic); + }); + } + + let panelEventPromise = new Promise(resolve => { + win.PopupNotifications.panel.addEventListener( + "PanelUpdated", + function eventListener(e) { + // Skip notifications that are not the one that we are supposed to be looking for + if (!e.detail.includes(aId)) { + return; + } + win.PopupNotifications.panel.removeEventListener( + "PanelUpdated", + eventListener + ); + resolve(); + } + ); + }); + + await observerPromise; + await panelEventPromise; + await waitForTick(); + + info("Saw a " + aId + " notification"); + ok(win.PopupNotifications.isPanelOpen, "Panel should be open"); + is( + win.PopupNotifications.panel.childNodes.length, + aExpectedCount, + "Should be the right number of notifications" + ); + if (win.PopupNotifications.panel.childNodes.length) { + let nodes = Array.from(win.PopupNotifications.panel.childNodes); + let notification = nodes.find(n => n.id == aId + "-notification"); + ok(notification, "Should have seen the " + aId + " notification"); + + let n = win.PopupNotifications.getNotification(aId); + is( + n?.anchorElement?.id || n?.anchorElement?.parentElement?.id, + expectedAnchorID, + "expected the right anchor ID" + ); + } + await SimpleTest.promiseFocus(win.PopupNotifications.window); + + return win.PopupNotifications.panel; +} + +function waitForNotificationClose(win = window) { + if (!win.PopupNotifications.isPanelOpen) { + return Promise.resolve(); + } + return new Promise(resolve => { + info("Waiting for notification to close"); + win.PopupNotifications.panel.addEventListener( + "popuphidden", + function () { + resolve(); + }, + { once: true } + ); + }); +} + +async function waitForInstallDialog(id = "addon-webext-permissions") { + let panel = await waitForNotification(id); + return panel.childNodes[0]; +} + +function removeTabAndWaitForNotificationClose() { + let closePromise = waitForNotificationClose(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + return closePromise; +} + +function acceptInstallDialog(installDialog) { + installDialog.button.click(); +} + +async function waitForSingleNotification(aCallback) { + while (PopupNotifications.panel.childNodes.length != 1) { + await new Promise(resolve => executeSoon(resolve)); + + info("Waiting for single notification"); + // Notification should never close while we wait + ok(PopupNotifications.isPanelOpen, "Notification should still be open"); + } +} + +function setupRedirect(aSettings) { + var url = + "https://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/redirect.sjs?mode=setup"; + for (var name in aSettings) { + url += "&" + name + "=" + aSettings[name]; + } + + var req = new XMLHttpRequest(); + req.open("GET", url, false); + req.send(null); +} + +var TESTS = [ + async function test_disabledInstall() { + await SpecialPowers.pushPrefEnv({ + set: [["xpinstall.enabled", false]], + }); + let notificationPromise = waitForNotification("xpinstall-disabled"); + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: "amosigned.xpi", + }) + ); + BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + let panel = await notificationPromise; + + let notification = panel.childNodes[0]; + is( + notification.button.label, + "Enable", + "Should have seen the right button" + ); + is( + notification.getAttribute("label"), + "Software installation is currently disabled. Click Enable and try again.", + "notification label is correct" + ); + + let closePromise = waitForNotificationClose(); + // Click on Enable + EventUtils.synthesizeMouseAtCenter(notification.button, {}); + await closePromise; + + try { + ok( + Services.prefs.getBoolPref("xpinstall.enabled"), + "Installation should be enabled" + ); + } catch (e) { + ok(false, "xpinstall.enabled should be set"); + } + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + let installs = await AddonManager.getAllInstalls(); + is(installs.length, 0, "Shouldn't be any pending installs"); + await SpecialPowers.popPrefEnv(); + }, + + async function test_blockedInstall() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.postDownloadThirdPartyPrompt", false]], + }); + + let notificationPromise = waitForNotification("addon-install-blocked"); + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: "amosigned.xpi", + }) + ); + BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + let panel = await notificationPromise; + + let notification = panel.childNodes[0]; + is( + notification.button.label, + "Continue to Installation", + "Should have seen the right button" + ); + is( + notification + .querySelector("#addon-install-blocked-info") + .getAttribute("href"), + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "unlisted-extensions-risks", + "Got the expected SUMO page as a learn more link in the addon-install-blocked panel" + ); + let message = panel.ownerDocument.getElementById( + "addon-install-blocked-message" + ); + is( + message.textContent, + "You are attempting to install an add-on from example.com. Make sure you trust this site before continuing.", + "Should have seen the right message" + ); + + let dialogPromise = waitForInstallDialog(); + // Click on Allow + EventUtils.synthesizeMouse(notification.button, 20, 10, {}); + + // Notification should have changed to progress notification + ok(PopupNotifications.isPanelOpen, "Notification should still be open"); + notification = panel.childNodes[0]; + is( + notification.id, + "addon-progress-notification", + "Should have seen the progress notification" + ); + + let installDialog = await dialogPromise; + + notificationPromise = acceptAppMenuNotificationWhenShown( + "addon-installed", + "amosigned-xpi@tests.mozilla.org" + ); + + installDialog.button.click(); + await notificationPromise; + + let installs = await AddonManager.getAllInstalls(); + is(installs.length, 0, "Should be no pending installs"); + + let addon = await AddonManager.getAddonByID( + "amosigned-xpi@tests.mozilla.org" + ); + await addon.uninstall(); + + await BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); + }, + + async function test_blockedInstallDomain() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.postDownloadThirdPartyPrompt", true], + ["extensions.install_origins.enabled", true], + ], + }); + + let progressPromise = waitForProgressNotification(); + let notificationPromise = waitForNotification("addon-install-failed"); + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: TESTROOT2 + "webmidi_permission.xpi", + }) + ); + BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + await progressPromise; + let panel = await notificationPromise; + + let notification = panel.childNodes[0]; + is( + notification.getAttribute("label"), + "The add-on WebMIDI test addon can not be installed from this location.", + "Should have seen the right message" + ); + + await removeTabAndWaitForNotificationClose(); + await SpecialPowers.popPrefEnv(); + }, + + async function test_allowedInstallDomain() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.postDownloadThirdPartyPrompt", true], + ["extensions.install_origins.enabled", true], + ], + }); + + let notificationPromise = waitForNotification("addon-install-blocked"); + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: TESTROOT + "webmidi_permission.xpi", + }) + ); + BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + let panel = await notificationPromise; + + let notification = panel.childNodes[0]; + is( + notification.button.label, + "Continue to Installation", + "Should have seen the right button" + ); + let message = panel.ownerDocument.getElementById( + "addon-install-blocked-message" + ); + is( + message.textContent, + "You are attempting to install an add-on from example.com. Make sure you trust this site before continuing.", + "Should have seen the right message" + ); + + // Next we get the permissions prompt, which also warns of the unsigned state of the addon + notificationPromise = waitForNotification("addon-webext-permissions"); + // Click on Allow on the 3rd party panel + notification.button.click(); + panel = await notificationPromise; + notification = panel.childNodes[0]; + + is(notification.button.label, "Add", "Should have seen the right button"); + + is( + notification.id, + "addon-webext-permissions-notification", + "Should have seen the permissions panel" + ); + let singlePerm = panel.ownerDocument.getElementById( + "addon-webext-perm-single-entry" + ); + is( + singlePerm.textContent, + "Access MIDI devices", + "Should have seen the right permission text" + ); + + notificationPromise = acceptAppMenuNotificationWhenShown( + "addon-installed", + "webmidi@test.mozilla.org", + { incognitoHidden: false, checkIncognito: true } + ); + + // Click on Allow on the permissions panel + notification.button.click(); + + await notificationPromise; + + let installs = await AddonManager.getAllInstalls(); + is(installs.length, 0, "Should be no pending installs"); + + let addon = await AddonManager.getAddonByID("webmidi@test.mozilla.org"); + await TestUtils.topicObserved("webextension-sitepermissions-startup"); + + // This addon should have a site permission with private browsing. + let uri = Services.io.newURI(addon.siteOrigin); + let pbPrincipal = Services.scriptSecurityManager.createContentPrincipal( + uri, + { + privateBrowsingId: 1, + } + ); + let permission = Services.perms.testExactPermissionFromPrincipal( + pbPrincipal, + "midi" + ); + is( + permission, + Services.perms.ALLOW_ACTION, + "api access in private browsing granted" + ); + + await addon.uninstall(); + + // Verify the permission has not been retained. + let { permissions } = await ExtensionPermissions.get( + "webmidi@test.mozilla.org" + ); + ok( + !permissions.includes("internal:privateBrowsingAllowed"), + "permission is not set after uninstall" + ); + + await BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); + }, + + async function test_blockedPostDownload() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.postDownloadThirdPartyPrompt", true]], + }); + + let notificationPromise = waitForNotification("addon-install-blocked"); + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: "amosigned.xpi", + }) + ); + BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + let panel = await notificationPromise; + + let notification = panel.childNodes[0]; + is( + notification.button.label, + "Continue to Installation", + "Should have seen the right button" + ); + let message = panel.ownerDocument.getElementById( + "addon-install-blocked-message" + ); + is( + message.textContent, + "You are attempting to install an add-on from example.com. Make sure you trust this site before continuing.", + "Should have seen the right message" + ); + + let dialogPromise = waitForInstallDialog(); + // Click on Allow + EventUtils.synthesizeMouse(notification.button, 20, 10, {}); + + let installDialog = await dialogPromise; + + notificationPromise = acceptAppMenuNotificationWhenShown( + "addon-installed", + "amosigned-xpi@tests.mozilla.org" + ); + + installDialog.button.click(); + await notificationPromise; + + let installs = await AddonManager.getAllInstalls(); + is(installs.length, 0, "Should be no pending installs"); + + let addon = await AddonManager.getAddonByID( + "amosigned-xpi@tests.mozilla.org" + ); + await addon.uninstall(); + + await BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); + }, + + async function test_recommendedPostDownload() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.postDownloadThirdPartyPrompt", true]], + }); + + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: "recommended.xpi", + }) + ); + BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + + let installDialog = await waitForInstallDialog(); + + let notificationPromise = acceptAppMenuNotificationWhenShown( + "addon-installed", + "{811d77f1-f306-4187-9251-b4ff99bad60b}" + ); + + installDialog.button.click(); + await notificationPromise; + + let installs = await AddonManager.getAllInstalls(); + is(installs.length, 0, "Should be no pending installs"); + + let addon = await AddonManager.getAddonByID( + "{811d77f1-f306-4187-9251-b4ff99bad60b}" + ); + await addon.uninstall(); + + await BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); + }, + + async function test_priviledgedNo3rdPartyPrompt() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.postDownloadThirdPartyPrompt", true]], + }); + AddonManager.checkUpdateSecurity = false; + registerCleanupFunction(() => { + AddonManager.checkUpdateSecurity = true; + }); + + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: "privileged.xpi", + }) + ); + + let installDialogPromise = waitForInstallDialog(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + + let notificationPromise = acceptAppMenuNotificationWhenShown( + "addon-installed", + "test@tests.mozilla.org", + { incognitoHidden: true } + ); + + (await installDialogPromise).button.click(); + await notificationPromise; + + let installs = await AddonManager.getAllInstalls(); + is(installs.length, 0, "Should be no pending installs"); + + let addon = await AddonManager.getAddonByID("test@tests.mozilla.org"); + await addon.uninstall(); + + await BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); + AddonManager.checkUpdateSecurity = true; + }, + + async function test_permaBlockInstall() { + let notificationPromise = waitForNotification("addon-install-blocked"); + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: "amosigned.xpi", + }) + ); + let target = TESTROOT + "installtrigger.html?" + triggers; + + BrowserTestUtils.openNewForegroundTab(gBrowser, target); + let notification = (await notificationPromise).firstElementChild; + let neverAllowBtn = notification.menupopup.firstElementChild; + + neverAllowBtn.click(); + + await TestUtils.waitForCondition( + () => !PopupNotifications.isPanelOpen, + "Waiting for notification to close" + ); + + let installs = await AddonManager.getAllInstalls(); + is(installs.length, 0, "Should be no pending installs"); + + let installPerm = PermissionTestUtils.testPermission( + gBrowser.currentURI, + "install" + ); + is( + installPerm, + Ci.nsIPermissionManager.DENY_ACTION, + "Addon installation should be blocked for site" + ); + + await BrowserTestUtils.removeTab(gBrowser.selectedTab); + + PermissionTestUtils.remove(target, "install"); + }, + + async function test_permaBlockedInstallNoPrompt() { + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: "amosigned.xpi", + }) + ); + let target = TESTROOT + "installtrigger.html?" + triggers; + + PermissionTestUtils.add(target, "install", Services.perms.DENY_ACTION); + await BrowserTestUtils.openNewForegroundTab(gBrowser, target); + + let panelOpened; + try { + panelOpened = await TestUtils.waitForCondition( + () => PopupNotifications.isPanelOpen, + 100, + 10 + ); + } catch (ex) { + panelOpened = false; + } + is(panelOpened, false, "Addon prompt should not open"); + + let installs = await AddonManager.getAllInstalls(); + is(installs.length, 0, "Should be no pending installs"); + + await BrowserTestUtils.removeTab(gBrowser.selectedTab); + + PermissionTestUtils.remove(target, "install"); + }, + + async function test_whitelistedInstall() { + let originalTab = gBrowser.selectedTab; + let tab; + gBrowser.selectedTab = originalTab; + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + let progressPromise = waitForProgressNotification(); + let dialogPromise = waitForInstallDialog(); + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: "amosigned.xpi", + }) + ); + BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ).then(newTab => (tab = newTab)); + await progressPromise; + let installDialog = await dialogPromise; + await BrowserTestUtils.waitForCondition( + () => !!tab, + "tab should be present" + ); + + is( + gBrowser.selectedTab, + tab, + "tab selected in response to the addon-install-confirmation notification" + ); + + let notificationPromise = acceptAppMenuNotificationWhenShown( + "addon-installed", + "amosigned-xpi@tests.mozilla.org", + { dismiss: true } + ); + acceptInstallDialog(installDialog); + await notificationPromise; + + let installs = await AddonManager.getAllInstalls(); + is(installs.length, 0, "Should be no pending installs"); + + let addon = await AddonManager.getAddonByID( + "amosigned-xpi@tests.mozilla.org" + ); + + // Test that the addon does not have permission. Reload it to ensure it would + // have been set if possible. + await addon.reload(); + let policy = WebExtensionPolicy.getByID(addon.id); + ok( + !policy.privateBrowsingAllowed, + "private browsing permission was not granted" + ); + + await addon.uninstall(); + + PermissionTestUtils.remove("http://example.com/", "install"); + + await removeTabAndWaitForNotificationClose(); + }, + + async function test_failedDownload() { + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + let progressPromise = waitForProgressNotification(); + let failPromise = waitForNotification("addon-install-failed"); + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: "missing.xpi", + }) + ); + BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + await progressPromise; + let panel = await failPromise; + + let notification = panel.childNodes[0]; + is( + notification.getAttribute("label"), + "The add-on could not be downloaded because of a connection failure.", + "Should have seen the right message" + ); + + PermissionTestUtils.remove("http://example.com/", "install"); + await removeTabAndWaitForNotificationClose(); + }, + + async function test_corruptFile() { + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + let progressPromise = waitForProgressNotification(); + let failPromise = waitForNotification("addon-install-failed"); + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: "corrupt.xpi", + }) + ); + BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + await progressPromise; + let panel = await failPromise; + + let notification = panel.childNodes[0]; + is( + notification.getAttribute("label"), + "The add-on downloaded from this site could not be installed " + + "because it appears to be corrupt.", + "Should have seen the right message" + ); + + PermissionTestUtils.remove("http://example.com/", "install"); + await removeTabAndWaitForNotificationClose(); + }, + + async function test_incompatible() { + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + let progressPromise = waitForProgressNotification(); + let failPromise = waitForNotification("addon-install-failed"); + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: "incompatible.xpi", + }) + ); + BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + await progressPromise; + let panel = await failPromise; + + let notification = panel.childNodes[0]; + let brandBundle = Services.strings.createBundle( + "chrome://branding/locale/brand.properties" + ); + let brandShortName = brandBundle.GetStringFromName("brandShortName"); + let message = `XPI Test could not be installed because it is not compatible with ${brandShortName} ${Services.appinfo.version}.`; + is( + notification.getAttribute("label"), + message, + "Should have seen the right message" + ); + + PermissionTestUtils.remove("http://example.com/", "install"); + await removeTabAndWaitForNotificationClose(); + }, + + async function test_localFile() { + let cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService( + Ci.nsIChromeRegistry + ); + let path; + try { + path = cr.convertChromeURL(makeURI(CHROMEROOT + "corrupt.xpi")).spec; + } catch (ex) { + path = CHROMEROOT + "corrupt.xpi"; + } + + let failPromise = new Promise(resolve => { + Services.obs.addObserver(function observer() { + Services.obs.removeObserver(observer, "addon-install-failed"); + resolve(); + }, "addon-install-failed"); + }); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + BrowserTestUtils.startLoadingURIString(gBrowser, path); + await failPromise; + + // Wait for the browser code to add the failure notification + await waitForSingleNotification(); + + let notification = PopupNotifications.panel.childNodes[0]; + is( + notification.id, + "addon-install-failed-notification", + "Should have seen the install fail" + ); + is( + notification.getAttribute("label"), + "This add-on could not be installed because it appears to be corrupt.", + "Should have seen the right message" + ); + + await removeTabAndWaitForNotificationClose(); + }, + + async function test_urlBar() { + let progressPromise = waitForProgressNotification(); + let dialogPromise = waitForInstallDialog(); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + gURLBar.value = TESTROOT + "amosigned.xpi"; + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + + await progressPromise; + let installDialog = await dialogPromise; + + let notificationPromise = acceptAppMenuNotificationWhenShown( + "addon-installed", + "amosigned-xpi@tests.mozilla.org", + { checkIncognito: true } + ); + installDialog.button.click(); + await notificationPromise; + + let installs = await AddonManager.getAllInstalls(); + is(installs.length, 0, "Should be no pending installs"); + + let addon = await AddonManager.getAddonByID( + "amosigned-xpi@tests.mozilla.org" + ); + // The panel is reloading the addon due to the permission change, we need some way + // to wait for the reload to finish. addon.startupPromise doesn't do it for + // us, so we'll just restart again. + await addon.reload(); + + // This addon should have private browsing permission. + let policy = WebExtensionPolicy.getByID(addon.id); + ok(policy.privateBrowsingAllowed, "private browsing permission granted"); + + await addon.uninstall(); + + await removeTabAndWaitForNotificationClose(); + }, + + async function test_wrongHost() { + let requestedUrl = TESTROOT2 + "enabled.html"; + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + + let loadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + requestedUrl + ); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT2 + "enabled.html" + ); + await loadedPromise; + + let progressPromise = waitForProgressNotification(); + let notificationPromise = waitForNotification("addon-install-failed"); + BrowserTestUtils.startLoadingURIString(gBrowser, TESTROOT + "corrupt.xpi"); + await progressPromise; + let panel = await notificationPromise; + + let notification = panel.childNodes[0]; + is( + notification.getAttribute("label"), + "The add-on downloaded from this site could not be installed " + + "because it appears to be corrupt.", + "Should have seen the right message" + ); + + await removeTabAndWaitForNotificationClose(); + }, + + async function test_renotifyBlocked() { + let notificationPromise = waitForNotification("addon-install-blocked"); + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: "amosigned.xpi", + }) + ); + BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + let panel = await notificationPromise; + + let closePromise = waitForNotificationClose(); + // hide the panel (this simulates the user dismissing it) + panel.hidePopup(); + await closePromise; + + info("Timeouts after this probably mean bug 589954 regressed"); + + await new Promise(resolve => executeSoon(resolve)); + + notificationPromise = waitForNotification("addon-install-blocked"); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + await notificationPromise; + + let installs = await AddonManager.getAllInstalls(); + is(installs.length, 2, "Should be two pending installs"); + + await removeTabAndWaitForNotificationClose(gBrowser.selectedTab); + + installs = await AddonManager.getAllInstalls(); + is(installs.length, 0, "Should have cancelled the installs"); + }, + + async function test_cancel() { + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + let notificationPromise = waitForNotification(PROGRESS_NOTIFICATION); + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: "slowinstall.sjs?file=amosigned.xpi", + }) + ); + BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + let panel = await notificationPromise; + let notification = panel.childNodes[0]; + + ok(PopupNotifications.isPanelOpen, "Notification should still be open"); + is( + PopupNotifications.panel.childNodes.length, + 1, + "Should be only one notification" + ); + is( + notification.id, + "addon-progress-notification", + "Should have seen the progress notification" + ); + + // Cancel the download + let install = notification.notification.options.installs[0]; + let cancelledPromise = new Promise(resolve => { + install.addListener({ + onDownloadCancelled() { + install.removeListener(this); + resolve(); + }, + }); + }); + EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {}); + await cancelledPromise; + + await waitForTick(); + + ok(!PopupNotifications.isPanelOpen, "Notification should be closed"); + + let installs = await AddonManager.getAllInstalls(); + is(installs.length, 0, "Should be no pending install"); + + PermissionTestUtils.remove("http://example.com/", "install"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + }, + + async function test_failedSecurity() { + await SpecialPowers.pushPrefEnv({ + set: [ + [PREF_INSTALL_REQUIREBUILTINCERTS, false], + ["extensions.postDownloadThirdPartyPrompt", false], + ], + }); + + setupRedirect({ + Location: TESTROOT + "amosigned.xpi", + }); + + let notificationPromise = waitForNotification("addon-install-blocked"); + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: "redirect.sjs?mode=redirect", + }) + ); + BrowserTestUtils.openNewForegroundTab( + gBrowser, + SECUREROOT + "installtrigger.html?" + triggers + ); + let panel = await notificationPromise; + + let notification = panel.childNodes[0]; + // Click on Allow + EventUtils.synthesizeMouse(notification.button, 20, 10, {}); + + // Notification should have changed to progress notification + ok(PopupNotifications.isPanelOpen, "Notification should still be open"); + is( + PopupNotifications.panel.childNodes.length, + 1, + "Should be only one notification" + ); + notification = panel.childNodes[0]; + is( + notification.id, + "addon-progress-notification", + "Should have seen the progress notification" + ); + + // Wait for it to fail + await new Promise(resolve => { + Services.obs.addObserver(function observer() { + Services.obs.removeObserver(observer, "addon-install-failed"); + resolve(); + }, "addon-install-failed"); + }); + + // Allow the browser code to add the failure notification and then wait + // for the progress notification to dismiss itself + await waitForSingleNotification(); + is( + PopupNotifications.panel.childNodes.length, + 1, + "Should be only one notification" + ); + notification = panel.childNodes[0]; + is( + notification.id, + "addon-install-failed-notification", + "Should have seen the install fail" + ); + + await removeTabAndWaitForNotificationClose(); + await SpecialPowers.popPrefEnv(); + }, + + async function test_incognito_checkbox() { + // Grant permission up front. + const permissionName = "internal:privateBrowsingAllowed"; + let incognitoPermission = { + permissions: [permissionName], + origins: [], + }; + await ExtensionPermissions.add( + "amosigned-xpi@tests.mozilla.org", + incognitoPermission + ); + + let progressPromise = waitForProgressNotification(); + let dialogPromise = waitForInstallDialog(); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + gURLBar.value = TESTROOT + "amosigned.xpi"; + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + + await progressPromise; + let installDialog = await dialogPromise; + + let notificationPromise = acceptAppMenuNotificationWhenShown( + "addon-installed", + "amosigned-xpi@tests.mozilla.org", + { incognitoChecked: true } + ); + installDialog.button.click(); + await notificationPromise; + + let installs = await AddonManager.getAllInstalls(); + is(installs.length, 0, "Should be no pending installs"); + + let addon = await AddonManager.getAddonByID( + "amosigned-xpi@tests.mozilla.org" + ); + // The panel is reloading the addon due to the permission change, we need some way + // to wait for the reload to finish. addon.startupPromise doesn't do it for + // us, so we'll just restart again. + await AddonTestUtils.promiseWebExtensionStartup( + "amosigned-xpi@tests.mozilla.org" + ); + + // This addon should no longer have private browsing permission. + let policy = WebExtensionPolicy.getByID(addon.id); + ok(!policy.privateBrowsingAllowed, "private browsing permission removed"); + + await addon.uninstall(); + + await removeTabAndWaitForNotificationClose(); + }, + + async function test_incognito_checkbox_new_window() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(win); + // Grant permission up front. + const permissionName = "internal:privateBrowsingAllowed"; + let incognitoPermission = { + permissions: [permissionName], + origins: [], + }; + await ExtensionPermissions.add( + "amosigned-xpi@tests.mozilla.org", + incognitoPermission + ); + + let panelEventPromise = new Promise(resolve => { + win.PopupNotifications.panel.addEventListener( + "PanelUpdated", + function eventListener(e) { + if (e.detail.includes("addon-webext-permissions")) { + win.PopupNotifications.panel.removeEventListener( + "PanelUpdated", + eventListener + ); + resolve(); + } + } + ); + }); + + win.gBrowser.selectedTab = BrowserTestUtils.addTab( + win.gBrowser, + "about:blank" + ); + await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + win.gURLBar.value = TESTROOT + "amosigned.xpi"; + win.gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter", {}, win); + + await panelEventPromise; + await waitForTick(); + + let panel = win.PopupNotifications.panel; + let installDialog = panel.childNodes[0]; + + let notificationPromise = acceptAppMenuNotificationWhenShown( + "addon-installed", + "amosigned-xpi@tests.mozilla.org", + { incognitoChecked: true, global: win } + ); + acceptInstallDialog(installDialog); + await notificationPromise; + + let installs = await AddonManager.getAllInstalls(); + is(installs.length, 0, "Should be no pending installs"); + + let addon = await AddonManager.getAddonByID( + "amosigned-xpi@tests.mozilla.org" + ); + // The panel is reloading the addon due to the permission change, we need some way + // to wait for the reload to finish. addon.startupPromise doesn't do it for + // us, so we'll just restart again. + await AddonTestUtils.promiseWebExtensionStartup( + "amosigned-xpi@tests.mozilla.org" + ); + + // This addon should no longer have private browsing permission. + let policy = WebExtensionPolicy.getByID(addon.id); + ok(!policy.privateBrowsingAllowed, "private browsing permission removed"); + + await addon.uninstall(); + + await BrowserTestUtils.closeWindow(win); + }, + + async function test_blockedInstallDomain_with_unified_extensions() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.install_origins.enabled", true]], + }); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(win); + + let progressPromise = waitForProgressNotification( + false, + 1, + true, + "unified-extensions-button", + win + ); + let notificationPromise = waitForNotification( + "addon-install-failed", + 1, + "unified-extensions-button", + win + ); + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: TESTROOT2 + "webmidi_permission.xpi", + }) + ); + await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + await progressPromise; + await notificationPromise; + + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); + }, + + async function test_mv3_installOrigins_disallowed_with_unified_extensions() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Disable signature check because we load an unsigned MV3 extension. + ["xpinstall.signatures.required", false], + ["extensions.install_origins.enabled", true], + ], + }); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(win); + + let notificationPromise = waitForNotification( + "addon-install-failed", + 1, + "unified-extensions-button", + win + ); + let triggers = encodeURIComponent( + JSON.stringify({ + // This XPI does not have any `install_origins` in its manifest. + XPI: "unsigned_mv3.xpi", + }) + ); + await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + await notificationPromise; + + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); + }, + + async function test_mv3_installOrigins_allowed_with_unified_extensions() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Disable signature check because we load an unsigned MV3 extension. + ["xpinstall.signatures.required", false], + // When this pref is disabled, install should be possible. + ["extensions.install_origins.enabled", false], + ], + }); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(win); + + let notificationPromise = waitForNotification( + "addon-install-blocked", + 1, + "unified-extensions-button", + win + ); + let triggers = encodeURIComponent( + JSON.stringify({ + // This XPI does not have any `install_origins` in its manifest. + XPI: "unsigned_mv3.xpi", + }) + ); + await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + let panel = await notificationPromise; + + let closePromise = waitForNotificationClose(win); + // hide the panel (this simulates the user dismissing it) + panel.hidePopup(); + await closePromise; + + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); + }, +]; + +var gTestStart = null; + +var XPInstallObserver = { + observe(aSubject, aTopic, aData) { + var installInfo = aSubject.wrappedJSObject; + info( + "Observed " + aTopic + " for " + installInfo.installs.length + " installs" + ); + installInfo.installs.forEach(function (aInstall) { + info( + "Install of " + + aInstall.sourceURI.spec + + " was in state " + + aInstall.state + ); + }); + }, +}; + +add_task(async function () { + requestLongerTimeout(4); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.logging.enabled", true], + ["extensions.strictCompatibility", true], + ["extensions.install.requireSecureOrigin", false], + ["security.dialog_enable_delay", 0], + // These tests currently depends on InstallTrigger.install. + ["extensions.InstallTrigger.enabled", true], + ["extensions.InstallTriggerImpl.enabled", true], + // Relax the user input requirements while running this test. + ["xpinstall.userActivation.required", false], + ], + }); + + Services.obs.addObserver(XPInstallObserver, "addon-install-started"); + Services.obs.addObserver(XPInstallObserver, "addon-install-blocked"); + Services.obs.addObserver(XPInstallObserver, "addon-install-failed"); + + registerCleanupFunction(async function () { + // Make sure no more test parts run in case we were timed out + TESTS = []; + + let aInstalls = await AddonManager.getAllInstalls(); + aInstalls.forEach(function (aInstall) { + aInstall.cancel(); + }); + + Services.obs.removeObserver(XPInstallObserver, "addon-install-started"); + Services.obs.removeObserver(XPInstallObserver, "addon-install-blocked"); + Services.obs.removeObserver(XPInstallObserver, "addon-install-failed"); + }); + + for (let i = 0; i < TESTS.length; ++i) { + if (gTestStart) { + info("Test part took " + (Date.now() - gTestStart) + "ms"); + } + + ok(!PopupNotifications.isPanelOpen, "Notification should be closed"); + + let installs = await AddonManager.getAllInstalls(); + + is(installs.length, 0, "Should be no active installs"); + info("Running " + TESTS[i].name); + gTestStart = Date.now(); + await TESTS[i](); + } +}); diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_empty.js b/toolkit/mozapps/extensions/test/xpinstall/browser_empty.js new file mode 100644 index 0000000000..d9739a0dcf --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_empty.js @@ -0,0 +1,39 @@ +// ---------------------------------------------------------------------------- +// Test whether an install fails when there is no install script present. +function test() { + // This test currently depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.downloadFailedCallback = download_failed; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Empty XPI": TESTROOT + "empty.xpi", + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function download_failed(install) { + is(install.error, AddonManager.ERROR_CORRUPT_FILE, "Install should fail"); +} + +function finish_test(count) { + is(count, 0, "No add-ons should have been installed"); + PermissionTestUtils.remove("http://example.com", "install"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_enabled.js b/toolkit/mozapps/extensions/test/xpinstall/browser_enabled.js new file mode 100644 index 0000000000..b8ee1b254f --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_enabled.js @@ -0,0 +1,103 @@ +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.InstallTrigger.enabled", true], + ["extensions.InstallTriggerImpl.enabled", true], + // Relax the user input requirements while running this test. + ["xpinstall.userActivation.required", false], + ], + }); +}); + +// Test whether an InstallTrigger.enabled is working +add_task(async function test_enabled() { + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "enabled.html" + ); + + let text = await ContentTask.spawn( + gBrowser.selectedBrowser, + undefined, + () => content.document.getElementById("enabled").textContent + ); + + is(text, "true", "installTrigger should have been enabled"); + gBrowser.removeCurrentTab(); +}); + +// Test whether an InstallTrigger.enabled is working +add_task(async function test_disabled() { + Services.prefs.setBoolPref("xpinstall.enabled", false); + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "enabled.html" + ); + + let text = await ContentTask.spawn( + gBrowser.selectedBrowser, + undefined, + () => content.document.getElementById("enabled").textContent + ); + + is(text, "false", "installTrigger should have not been enabled"); + Services.prefs.clearUserPref("xpinstall.enabled"); + gBrowser.removeCurrentTab(); +}); + +// Test whether an InstallTrigger.install call fails when xpinstall is disabled +add_task(async function test_disabled2() { + let installDisabledCalled = false; + + Harness.installDisabledCallback = installInfo => { + installDisabledCalled = true; + ok(true, "Saw installation disabled"); + }; + + Harness.installBlockedCallback = installInfo => { + ok(false, "Should never see the blocked install notification"); + return false; + }; + + Harness.installConfirmCallback = panel => { + ok(false, "Should never see an install confirmation dialog"); + return false; + }; + + Harness.setup(); + Services.prefs.setBoolPref("xpinstall.enabled", false); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": TESTROOT + "amosigned.xpi", + }) + ); + + BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + + await BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "InstallTriggered", + true, + undefined, + true + ); + + let text = await ContentTask.spawn( + gBrowser.selectedBrowser, + undefined, + () => content.document.getElementById("return").textContent + ); + + is(text, "false", "installTrigger should have not been enabled"); + ok(installDisabledCalled, "installDisabled callback was called"); + + Services.prefs.clearUserPref("xpinstall.enabled"); + gBrowser.removeCurrentTab(); + Harness.finish(); +}); diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_hash.js b/toolkit/mozapps/extensions/test/xpinstall/browser_hash.js new file mode 100644 index 0000000000..ab7d21b64e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_hash.js @@ -0,0 +1,47 @@ +// ---------------------------------------------------------------------------- +// Test whether an install succeeds when a valid hash is included +// This verifies bug 302284 +function test() { + // This test currently depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: TESTROOT + "amosigned.xpi", + Hash: "sha1:ee95834ad862245a9ef99ccecc2a857cadc16404", + toString() { + return this.URL; + }, + }, + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + PermissionTestUtils.remove("http://example.com", "install"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_hash2.js b/toolkit/mozapps/extensions/test/xpinstall/browser_hash2.js new file mode 100644 index 0000000000..9fd0c66292 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_hash2.js @@ -0,0 +1,47 @@ +// ---------------------------------------------------------------------------- +// Test whether an install succeeds using case-insensitive hashes +// This verifies bug 603021 +function test() { + // This test currently depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: TESTROOT + "amosigned.xpi", + Hash: "sha1:EE95834AD862245A9EF99CCECC2A857CADC16404", + toString() { + return this.URL; + }, + }, + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + PermissionTestUtils.remove("http://example.com", "install"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash.js b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash.js new file mode 100644 index 0000000000..1ce8eb55af --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash.js @@ -0,0 +1,55 @@ +// ---------------------------------------------------------------------------- +// Test whether an install succeeds when a valid hash is included in the HTTPS +// request +// This verifies bug 591070 +function test() { + // This test currently depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false); + + var url = "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs"; + url += + "?sha1:ee95834ad862245a9ef99ccecc2a857cadc16404|" + + TESTROOT + + "amosigned.xpi"; + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: url, + toString() { + return this.URL; + }, + }, + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + PermissionTestUtils.remove("http://example.com", "install"); + Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash2.js b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash2.js new file mode 100644 index 0000000000..56014b0a3e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash2.js @@ -0,0 +1,52 @@ +// ---------------------------------------------------------------------------- +// Test whether an install fails when a invalid hash is included in the HTTPS +// request +// This verifies bug 591070 +function test() { + // This test currently depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.downloadFailedCallback = download_failed; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false); + + var url = "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs"; + url += "?sha1:foobar|" + TESTROOT + "amosigned.xpi"; + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: url, + toString() { + return this.URL; + }, + }, + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function download_failed(install) { + is(install.error, AddonManager.ERROR_INCORRECT_HASH, "Download should fail"); +} + +function finish_test(count) { + is(count, 0, "0 Add-ons should have been successfully installed"); + + PermissionTestUtils.remove("http://example.com", "install"); + Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash3.js b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash3.js new file mode 100644 index 0000000000..ffb5a3ddb4 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash3.js @@ -0,0 +1,52 @@ +// ---------------------------------------------------------------------------- +// Tests that the HTTPS hash is ignored when InstallTrigger is passed a hash. +// This verifies bug 591070 +function test() { + // This test currently depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false); + + var url = "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs"; + url += "?sha1:foobar|" + TESTROOT + "amosigned.xpi"; + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: url, + Hash: "sha1:ee95834ad862245a9ef99ccecc2a857cadc16404", + toString() { + return this.URL; + }, + }, + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + PermissionTestUtils.remove("http://example.com", "install"); + Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash4.js b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash4.js new file mode 100644 index 0000000000..4964d56443 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash4.js @@ -0,0 +1,49 @@ +// ---------------------------------------------------------------------------- +// Test that hashes are ignored in the headers of HTTP requests +// This verifies bug 591070 +function test() { + // This test currently depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var url = "http://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs"; + url += "?sha1:foobar|" + TESTROOT + "amosigned.xpi"; + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: url, + toString() { + return this.URL; + }, + }, + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + PermissionTestUtils.remove("http://example.com", "install"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash5.js b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash5.js new file mode 100644 index 0000000000..727f13180b --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash5.js @@ -0,0 +1,53 @@ +// ---------------------------------------------------------------------------- +// Test that only the first HTTPS hash is used +// This verifies bug 591070 +function test() { + // This test currently depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false); + + var url = "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs"; + url += "?sha1:ee95834ad862245a9ef99ccecc2a857cadc16404|"; + url += "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs"; + url += "?sha1:foobar|" + TESTROOT + "amosigned.xpi"; + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: url, + toString() { + return this.URL; + }, + }, + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + PermissionTestUtils.remove("http://example.com", "install"); + Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash6.js b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash6.js new file mode 100644 index 0000000000..81f35f2140 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash6.js @@ -0,0 +1,107 @@ +// ---------------------------------------------------------------------------- +// Tests that a new hash is accepted when restarting a failed download +// This verifies bug 593535 +function setup_redirect(aSettings) { + var url = + "https://example.com/browser/" + RELATIVE_DIR + "redirect.sjs?mode=setup"; + for (var name in aSettings) { + url += "&" + name + "=" + aSettings[name]; + } + + var req = new XMLHttpRequest(); + req.open("GET", url, false); + req.send(null); +} + +var gInstall = null; + +function test() { + // This test currently depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.downloadFailedCallback = download_failed; + Harness.installsCompletedCallback = finish_failed_download; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false); + + // Set up the redirect to give a bad hash + setup_redirect({ + "X-Target-Digest": "sha1:foo", + Location: "http://example.com/browser/" + RELATIVE_DIR + "amosigned.xpi", + }); + + var url = + "https://example.com/browser/" + + RELATIVE_DIR + + "redirect.sjs?mode=redirect"; + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: url, + toString() { + return this.URL; + }, + }, + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function download_failed(install) { + is( + install.error, + AddonManager.ERROR_INCORRECT_HASH, + "Should have seen a hash failure" + ); + // Stash the failed download while the harness cleans itself up + gInstall = install; +} + +function finish_failed_download() { + // Setup to track the successful re-download + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + // Give it the right hash this time + setup_redirect({ + "X-Target-Digest": "sha1:ee95834ad862245a9ef99ccecc2a857cadc16404", + Location: "http://example.com/browser/" + RELATIVE_DIR + "amosigned.xpi", + }); + + // The harness expects onNewInstall events for all installs that are about to start + Harness.onNewInstall(gInstall); + + // Restart the install as a regular webpage install so the harness tracks it + AddonManager.installAddonFromWebpage( + "application/x-xpinstall", + gBrowser.selectedBrowser, + gBrowser.contentPrincipal, + gInstall + ); +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + PermissionTestUtils.remove("http://example.com", "install"); + Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_installchrome.js b/toolkit/mozapps/extensions/test/xpinstall/browser_installchrome.js new file mode 100644 index 0000000000..319214b66a --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_installchrome.js @@ -0,0 +1,36 @@ +// ---------------------------------------------------------------------------- +// Tests that calling InstallTrigger.installChrome works +function test() { + // This test depends on InstallTrigger.installChrome availability. + setInstallTriggerPrefs(); + + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + + "installchrome.html? " + + encodeURIComponent(TESTROOT + "amosigned.xpi") + ); +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + PermissionTestUtils.remove("http://example.com", "install"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile.js b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile.js new file mode 100644 index 0000000000..65ae80bd92 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile.js @@ -0,0 +1,42 @@ +// ---------------------------------------------------------------------------- +// Tests installing an local file works when loading the url +function test() { + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + var cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService( + Ci.nsIChromeRegistry + ); + + var chromeroot = extractChromeRoot(gTestPath); + var xpipath = chromeroot + "unsigned.xpi"; + try { + xpipath = cr.convertChromeURL(makeURI(chromeroot + "amosigned.xpi")).spec; + } catch (ex) { + // scenario where we are running from a .jar and already extracted + } + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => { + BrowserTestUtils.startLoadingURIString(gBrowser, xpipath); + }); +} + +function install_ended(install, addon) { + Assert.deepEqual( + install.installTelemetryInfo, + { source: "file-url" }, + "Got the expected install.installTelemetryInfo" + ); + + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} +// ---------------------------------------------------------------------------- diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile2.js b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile2.js new file mode 100644 index 0000000000..bfd52b18b9 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile2.js @@ -0,0 +1,61 @@ +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.InstallTrigger.enabled", true], + ["extensions.InstallTriggerImpl.enabled", true], + ], + }); +}); + +// ---------------------------------------------------------------------------- +// Test whether an install fails if the url is a local file when requested from +// web content +add_task(async function test() { + var cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService( + Ci.nsIChromeRegistry + ); + + var chromeroot = getChromeRoot(gTestPath); + var xpipath = chromeroot + "amosigned.xpi"; + try { + xpipath = cr.convertChromeURL(makeURI(xpipath)).spec; + } catch (ex) { + // scenario where we are running from a .jar and already extracted + } + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": xpipath, + }) + ); + + // In non-e10s the exception in the content page would trigger a test failure + if (!gMultiProcessBrowser) { + expectUncaughtException(); + } + + let URI = TESTROOT + "installtrigger.html?manualStartInstall" + triggers; + await BrowserTestUtils.withNewTab( + { gBrowser, url: URI }, + async function (browser) { + await SpecialPowers.spawn(browser, [], async function () { + let installTriggered = ContentTaskUtils.waitForEvent( + docShell.chromeEventHandler, + "InstallTriggered", + true, + null, + true + ); + content.wrappedJSObject.startInstall(); + await installTriggered; + let doc = content.document; + is( + doc.getElementById("return").textContent, + "exception", + "installTrigger should have failed" + ); + }); + } + ); +}); +// ---------------------------------------------------------------------------- diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js new file mode 100644 index 0000000000..c8d8532ed0 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js @@ -0,0 +1,42 @@ +// ---------------------------------------------------------------------------- +// Tests installing an add-on from a local file with whitelisting disabled. +// This should be blocked by the whitelist check. +function test() { + Harness.installBlockedCallback = allow_blocked; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + // Disable direct request whitelisting, installing from file should be blocked. + Services.prefs.setBoolPref("xpinstall.whitelist.directRequest", false); + + var cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService( + Ci.nsIChromeRegistry + ); + + var chromeroot = extractChromeRoot(gTestPath); + var xpipath = chromeroot + "amosigned.xpi"; + try { + xpipath = cr.convertChromeURL(makeURI(xpipath)).spec; + } catch (ex) { + // scenario where we are running from a .jar and already extracted + } + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => { + BrowserTestUtils.startLoadingURIString(gBrowser, xpipath); + }); +} + +function allow_blocked(installInfo) { + ok(true, "Seen blocked"); + return false; +} + +function finish_test(count) { + is(count, 0, "No add-ons should have been installed"); + + Services.prefs.clearUserPref("xpinstall.whitelist.directRequest"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js new file mode 100644 index 0000000000..771832a72b --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js @@ -0,0 +1,55 @@ +// ---------------------------------------------------------------------------- +// Tests installing an add-on from a local file with file origins disabled. +// This should be blocked by the origin allowed check. +function test() { + // This test currently depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + // prompt prior to download + SpecialPowers.pushPrefEnv({ + set: [["extensions.postDownloadThirdPartyPrompt", false]], + }); + + Harness.installBlockedCallback = allow_blocked; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + // Disable local file install, installing by file referrer should be blocked. + Services.prefs.setBoolPref("xpinstall.whitelist.fileRequest", false); + + var cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService( + Ci.nsIChromeRegistry + ); + + var chromeroot = extractChromeRoot(gTestPath); + var xpipath = chromeroot; + try { + xpipath = cr.convertChromeURL(makeURI(chromeroot)).spec; + } catch (ex) { + // scenario where we are running from a .jar and already extracted + } + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": TESTROOT + "amosigned.xpi", + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + xpipath + "installtrigger.html?" + triggers + ); +} + +function allow_blocked(installInfo) { + ok(true, "Seen blocked"); + return false; +} + +function finish_test(count) { + is(count, 0, "No add-ons should have been installed"); + + Services.prefs.clearUserPref("xpinstall.whitelist.fileRequest"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4_postDownload.js b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4_postDownload.js new file mode 100644 index 0000000000..8f8484a1c9 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4_postDownload.js @@ -0,0 +1,54 @@ +// ---------------------------------------------------------------------------- +// Tests installing an add-on from a local file with file origins disabled. +// This should be blocked by the origin allowed check. +function test() { + // This test currently depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installBlockedCallback = allow_blocked; + Harness.installsCompletedCallback = finish_test; + // Prevent the Harness from ending the test on download cancel. + Harness.downloadCancelledCallback = () => { + return false; + }; + Harness.setup(); + + // Disable local file install, installing by file referrer should be blocked. + Services.prefs.setBoolPref("xpinstall.whitelist.fileRequest", false); + + var cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService( + Ci.nsIChromeRegistry + ); + + var chromeroot = extractChromeRoot(gTestPath); + var xpipath = chromeroot; + try { + xpipath = cr.convertChromeURL(makeURI(chromeroot)).spec; + } catch (ex) { + // scenario where we are running from a .jar and already extracted + } + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": TESTROOT + "amosigned.xpi", + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + xpipath + "installtrigger.html?" + triggers + ); +} + +function allow_blocked(installInfo) { + ok(true, "Seen blocked"); + return false; +} + +function finish_test(count) { + is(count, 0, "No add-ons should have been installed"); + + Services.prefs.clearUserPref("xpinstall.whitelist.fileRequest"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_newwindow.js b/toolkit/mozapps/extensions/test/xpinstall/browser_newwindow.js new file mode 100644 index 0000000000..c4bc5c5d56 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_newwindow.js @@ -0,0 +1,89 @@ +// This functionality covered in this test is also covered in other tests. +// The purpose of this test is to catch window leaks. It should fail in +// debug builds if a window reference is held onto after an install finishes. +// See bug 1541577 for further details. + +let win; +let popupPromise; +let newtabPromise; +const exampleURI = Services.io.newURI("http://example.com"); +async function test() { + waitForExplicitFinish(); // have to call this ourselves because we're async. + + // This test currently depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", false]], + }); + Harness.installConfirmCallback = confirm_install; + Harness.installEndedCallback = install => { + return install.addon.uninstall(); + }; + Harness.installsCompletedCallback = finish_test; + Harness.finalContentEvent = "InstallComplete"; + win = await BrowserTestUtils.openNewBrowserWindow(); + Harness.setup(win); + + PermissionTestUtils.add(exampleURI, "install", Services.perms.ALLOW_ACTION); + + const triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: TESTROOT + "amosigned.xpi", + IconURL: TESTROOT + "icon.png", + }, + }) + ); + + const url = `${TESTROOT}installtrigger.html?${triggers}`; + newtabPromise = BrowserTestUtils.openNewForegroundTab(win.gBrowser, url); + popupPromise = BrowserTestUtils.waitForEvent( + win.PanelUI.notificationPanel, + "popupshown" + ); +} + +function confirm_install(panel) { + is(panel.getAttribute("name"), "XPI Test", "Should have seen the name"); + return true; +} + +async function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + PermissionTestUtils.remove(exampleURI, "install"); + + const results = await SpecialPowers.spawn( + win.gBrowser.selectedBrowser, + [], + () => { + return { + return: content.document.getElementById("return").textContent, + status: content.document.getElementById("status").textContent, + }; + } + ); + + is(results.return, "true", "installTrigger should have claimed success"); + is(results.status, "0", "Callback should have seen a success"); + + // Explicitly click the "OK" button to avoid the panel reopening in the other window once this + // window closes (see also bug 1535069): + await popupPromise; + win.PanelUI.notificationPanel + .querySelector("popupnotification[popupid=addon-installed]") + .button.click(); + + // Wait for the promise returned by BrowserTestUtils.openNewForegroundTab + // to be resolved before removing the window to prevent an uncaught exception + // triggered from inside openNewForegroundTab to trigger a test failure due + // to a race between openNewForegroundTab and closeWindow calls, e.g. as for + // Bug 1728482). + await newtabPromise; + + // Now finish the test: + await BrowserTestUtils.closeWindow(win); + Harness.finish(win); + win = null; +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_offline.js b/toolkit/mozapps/extensions/test/xpinstall/browser_offline.js new file mode 100644 index 0000000000..946ad1dff7 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_offline.js @@ -0,0 +1,82 @@ +var proxyPrefValue; + +// ---------------------------------------------------------------------------- +// Tests that going offline cancels an in progress download. +function test() { + // This test currently depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.downloadProgressCallback = download_progress; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": TESTROOT + "amosigned.xpi", + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function download_progress(addon, value, maxValue) { + try { + // Tests always connect to localhost, and per bug 87717, localhost is now + // reachable in offline mode. To avoid this, disable any proxy. + proxyPrefValue = Services.prefs.getIntPref("network.proxy.type"); + Services.prefs.setIntPref("network.proxy.type", 0); + Services.io.manageOfflineStatus = false; + Services.io.offline = true; + } catch (ex) {} +} + +function finish_test(count) { + function wait_for_online() { + info("Checking if the browser is still offline..."); + + let tab = gBrowser.selectedTab; + BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "DOMContentLoaded", + true + ).then(async function () { + let url = await ContentTask.spawn( + tab.linkedBrowser, + null, + async function () { + return content.document.documentURI; + } + ); + info("loaded: " + url); + if (/^about:neterror\?e=netOffline/.test(url)) { + wait_for_online(); + } else { + gBrowser.removeCurrentTab(); + Harness.finish(); + } + }); + BrowserTestUtils.startLoadingURIString( + tab.linkedBrowser, + "http://example.com/" + ); + } + + is(count, 0, "No add-ons should have been installed"); + try { + Services.prefs.setIntPref("network.proxy.type", proxyPrefValue); + Services.io.offline = false; + } catch (ex) {} + + PermissionTestUtils.remove("http://example.com", "install"); + + wait_for_online(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_privatebrowsing.js b/toolkit/mozapps/extensions/test/xpinstall/browser_privatebrowsing.js new file mode 100644 index 0000000000..9d55a3d8fa --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_privatebrowsing.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +let gDidSeeChannel = false; + +AddonTestUtils.initMochitest(this); + +function check_channel(subject) { + if (!(subject instanceof Ci.nsIHttpChannel)) { + return; + } + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + let uri = channel.URI; + if (!uri || !uri.spec.endsWith("amosigned.xpi")) { + return; + } + gDidSeeChannel = true; + ok(true, "Got request for " + uri.spec); + + let loadInfo = channel.loadInfo; + is( + loadInfo.originAttributes.privateBrowsingId, + 1, + "Request should have happened using private browsing" + ); +} +// ---------------------------------------------------------------------------- +// Tests we send the right cookies when installing through an InstallTrigger call +let gPrivateWin; +async function test() { + waitForExplicitFinish(); // have to call this ourselves because we're async. + + // This test currently depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first_pbm", false]], + }); + Harness.installConfirmCallback = confirm_install; + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.finalContentEvent = "InstallComplete"; + gPrivateWin = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + Harness.setup(gPrivateWin); + + let principal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI("http://example.com/"), + { privateBrowsingId: 1 } + ); + + PermissionTestUtils.add(principal, "install", Services.perms.ALLOW_ACTION); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: TESTROOT + "amosigned.xpi", + IconURL: TESTROOT + "icon.png", + toString() { + return this.URL; + }, + }, + }) + ); + gPrivateWin.gBrowser.selectedTab = BrowserTestUtils.addTab( + gPrivateWin.gBrowser + ); + Services.obs.addObserver(check_channel, "http-on-before-connect"); + BrowserTestUtils.startLoadingURIString( + gPrivateWin.gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function confirm_install(panel) { + is(panel.getAttribute("name"), "XPI Test", "Should have seen the name"); + return true; +} + +function install_ended(install, addon) { + AddonTestUtils.checkInstallInfo(install, { + method: "installTrigger", + source: "test-host", + sourceURL: /http:\/\/example.com\/.*\/installtrigger.html/, + }); + return addon.uninstall(); +} + +const finish_test = async function (count) { + ok( + gDidSeeChannel, + "Should have seen the request for the XPI and verified it was sent the right way." + ); + is(count, 1, "1 Add-on should have been successfully installed"); + + Services.obs.removeObserver(check_channel, "http-on-before-connect"); + + PermissionTestUtils.remove("http://example.com", "install"); + + const results = await SpecialPowers.spawn( + gPrivateWin.gBrowser.selectedBrowser, + [], + () => { + return { + return: content.document.getElementById("return").textContent, + status: content.document.getElementById("status").textContent, + }; + } + ); + + is(results.return, "true", "installTrigger should have claimed success"); + is(results.status, "0", "Callback should have seen a success"); + + await TestUtils.waitForCondition(() => + gPrivateWin.AppMenuNotifications._notifications.some( + n => n.id == "addon-installed" + ) + ); + // Explicitly remove the notification to avoid the panel reopening in the + // other window once this window closes (see also bug 1535069): + gPrivateWin.AppMenuNotifications.removeNotification("addon-installed"); + + // Now finish the test: + await BrowserTestUtils.closeWindow(gPrivateWin); + Harness.finish(gPrivateWin); + gPrivateWin = null; +}; diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_relative.js b/toolkit/mozapps/extensions/test/xpinstall/browser_relative.js new file mode 100644 index 0000000000..10d5df8b73 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_relative.js @@ -0,0 +1,67 @@ +// ---------------------------------------------------------------------------- +// Tests that InstallTrigger deals with relative urls correctly. +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installConfirmCallback = confirm_install; + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.finalContentEvent = "InstallComplete"; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: "amosigned.xpi", + IconURL: "icon.png", + toString() { + return this.URL; + }, + }, + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function confirm_install(panel) { + is(panel.getAttribute("name"), "XPI Test", "Should have seen the name"); + return true; +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +const finish_test = async function (count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + PermissionTestUtils.remove("http://example.com", "install"); + + const results = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + return { + return: content.document.getElementById("return").textContent, + status: content.document.getElementById("status").textContent, + }; + } + ); + + is(results.return, "true", "installTrigger should have claimed success"); + is(results.status, "0", "Callback should have seen a success"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +}; diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_required_useractivation.js b/toolkit/mozapps/extensions/test/xpinstall/browser_required_useractivation.js new file mode 100644 index 0000000000..6c8894699d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_required_useractivation.js @@ -0,0 +1,156 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +const XPI_URL = `${TESTROOT}amosigned.xpi`; + +async function runTestCase(spawnArgs, spawnFn, { expectInstall, clickLink }) { + await SpecialPowers.pushPrefEnv({ + set: [ + // Make use the user activation requirements is enabled while running this test. + ["xpinstall.userActivation.required", true], + ], + }); + await BrowserTestUtils.withNewTab(TESTROOT, async browser => { + const expectedError = `${XPI_URL} install cancelled because of missing user gesture activation`; + + let promiseDone; + + if (expectInstall) { + promiseDone = TestUtils.topicObserved("addon-install-blocked").then( + ([subject]) => { + // Cancel the pending installation flow. + subject.wrappedJSObject.cancel(); + } + ); + } else { + promiseDone = new Promise(resolve => { + function messageHandler(msgObj) { + if ( + msgObj instanceof Ci.nsIScriptError && + msgObj.message.includes(expectedError) + ) { + ok( + true, + "Expect error on triggering navigation to xpi without user gesture activation" + ); + cleanupListener(); + resolve(); + } + } + let listenerCleared = false; + function cleanupListener() { + if (!listenerCleared) { + Services.console.unregisterListener(messageHandler); + } + listenerCleared = true; + } + Services.console.registerListener(messageHandler); + registerCleanupFunction(cleanupListener); + }); + } + + await SpecialPowers.spawn(browser, spawnArgs, spawnFn); + + if (clickLink) { + info("Click link element"); + // Wait for the install to trigger the third party website doorhanger. + // Trigger the link by simulating a mouse click, and expect it to trigger the + // install flow instead (the window is still navigated to the xpi url from the + // webpage JS code, but doing it while handling a DOM event does make it pass + // the user activation check). + await BrowserTestUtils.synthesizeMouseAtCenter( + "#link-to-xpi-file", + {}, + browser + ); + } + + info("Wait test case to be completed"); + await promiseDone; + ok(true, "Test case run completed"); + }); +} + +add_task(async function testSuccessOnUserActivatedLink() { + await runTestCase( + [XPI_URL], + xpiURL => { + const { document } = this.content; + const link = document.createElement("a"); + link.id = "link-to-xpi-file"; + link.setAttribute("href", xpiURL); + link.textContent = "Link to XPI File"; + + // Empty the test case and add the link, if the link is not visible + // without scrolling, BrowserTestUtils.synthesizeMouseAtCenter may + // fail to trigger the mouse event. + document.body.innerHTML = ""; + document.body.appendChild(link); + }, + { expectInstall: true, clickLink: true } + ); +}); + +add_task(async function testSuccessOnJSWithUserActivation() { + await runTestCase( + [XPI_URL], + xpiURL => { + const { document } = this.content; + const link = document.createElement("a"); + link.id = "link-to-xpi-file"; + link.setAttribute("href", "#"); + link.textContent = "Link to XPI File"; + + // Empty the test case and add the link, if the link is not visible + // without scrolling, BrowserTestUtils.synthesizeMouseAtCenter may + // fail to trigger the mouse event. + document.body.innerHTML = ""; + document.body.appendChild(link); + + this.content.eval(` + const linkEl = document.querySelector("#link-to-xpi-file"); + linkEl.onclick = () => { + // This is expected to trigger the install flow successfully if handling + // a user gesture DOM event, but to fail when triggered outside of it (as + // done a few line below). + window.location = "${xpiURL}"; + }; + `); + }, + { expectInstall: true, clickLink: true } + ); +}); + +add_task(async function testFailureOnJSWithoutUserActivation() { + await runTestCase( + [XPI_URL], + xpiURL => { + this.content.eval(`window.location = "${xpiURL}";`); + }, + { expectInstall: false } + ); +}); + +add_task(async function testFailureOnJSWithoutUserActivation() { + await runTestCase( + [XPI_URL], + xpiURL => { + this.content.eval(` + const frame = document.createElement("iframe"); + frame.src = "${xpiURL}"; + document.body.innerHTML = ""; + document.body.appendChild(frame); + `); + }, + { expectInstall: false } + ); +}); diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_signed_url.js b/toolkit/mozapps/extensions/test/xpinstall/browser_signed_url.js new file mode 100644 index 0000000000..b01137927b --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_signed_url.js @@ -0,0 +1,32 @@ +// ---------------------------------------------------------------------------- +// Tests installing an signed add-on by navigating directly to the url +function test() { + Harness.installConfirmCallback = confirm_install; + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => { + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "amosigned.xpi" + ); + }); +} + +function confirm_install(panel) { + is(panel.getAttribute("name"), "XPI Test", "Should have seen the name"); + return true; +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + gBrowser.removeCurrentTab(); + Harness.finish(); +} +// ---------------------------------------------------------------------------- diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_softwareupdate.js b/toolkit/mozapps/extensions/test/xpinstall/browser_softwareupdate.js new file mode 100644 index 0000000000..76ddf0b7d3 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_softwareupdate.js @@ -0,0 +1,36 @@ +// ---------------------------------------------------------------------------- +// Tests that calling InstallTrigger.startSoftwareUpdate works +function test() { + // This test depends on InstallTrigger.startSoftwareUpdate availability. + setInstallTriggerPrefs(); + + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + + "startsoftwareupdate.html? " + + encodeURIComponent(TESTROOT + "amosigned.xpi") + ); +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + PermissionTestUtils.remove("http://example.com", "install"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_trigger_redirect.js b/toolkit/mozapps/extensions/test/xpinstall/browser_trigger_redirect.js new file mode 100644 index 0000000000..51e08e5001 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_trigger_redirect.js @@ -0,0 +1,48 @@ +// ---------------------------------------------------------------------------- +// Tests that the InstallTrigger callback can redirect to a relative url. +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installConfirmCallback = confirm_install; + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.finalContentEvent = "InstallComplete"; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "triggerredirect.html" + ); +} + +function confirm_install(panel) { + is(panel.getAttribute("name"), "XPI Test", "Should have seen the name"); + return true; +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + PermissionTestUtils.remove("http://example.com", "install"); + + is( + gBrowser.currentURI.spec, + TESTROOT + "triggerredirect.html#foo", + "Should have redirected" + ); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger.js b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger.js new file mode 100644 index 0000000000..cb81cf3d36 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger.js @@ -0,0 +1,68 @@ +// ---------------------------------------------------------------------------- +// Tests installing an unsigned add-on through an InstallTrigger call in web +// content. +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installConfirmCallback = confirm_install; + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.finalContentEvent = "InstallComplete"; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var triggers = encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: TESTROOT + "unsigned.xpi", + IconURL: TESTROOT + "icon.png", + toString() { + return this.URL; + }, + }, + }) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); +} + +function confirm_install(panel) { + is(panel.getAttribute("name"), "XPI Test", "Should have seen the name"); + return true; +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +const finish_test = async function (count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + PermissionTestUtils.remove("http://example.com", "install"); + + const results = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + return { + return: content.document.getElementById("return").textContent, + status: content.document.getElementById("status").textContent, + }; + } + ); + + is(results.return, "true", "installTrigger should have claimed success"); + is(results.status, "0", "Callback should have seen a success"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +}; diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_iframe.js b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_iframe.js new file mode 100644 index 0000000000..c98477d6e9 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_iframe.js @@ -0,0 +1,77 @@ +// ---------------------------------------------------------------------------- +// Test for bug 589598 - Ensure that installing through InstallTrigger +// works in an iframe in web content. + +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installConfirmCallback = confirm_install; + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.finalContentEvent = "InstallComplete"; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var inner_url = encodeURIComponent( + TESTROOT + + "installtrigger.html?" + + encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: TESTROOT + "unsigned.xpi", + IconURL: TESTROOT + "icon.png", + toString() { + return this.URL; + }, + }, + }) + ) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT + "installtrigger_frame.html?" + inner_url + ); +} + +function confirm_install(panel) { + is(panel.getAttribute("name"), "XPI Test", "Should have seen the name"); + return true; +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +const finish_test = async function (count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + PermissionTestUtils.remove("http://example.com", "install"); + + const results = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + return { + return: content.frames[0].document.getElementById("return").textContent, + status: content.frames[0].document.getElementById("status").textContent, + }; + } + ); + + is( + results.return, + "true", + "installTrigger in iframe should have claimed success" + ); + is(results.status, "0", "Callback in iframe should have seen a success"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +}; diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_xorigin.js b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_xorigin.js new file mode 100644 index 0000000000..7edbf318a0 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_xorigin.js @@ -0,0 +1,58 @@ +// ---------------------------------------------------------------------------- +// Ensure that an inner frame from a different origin can't initiate an install + +var wasOriginBlocked = false; + +function test() { + // This test depends on InstallTrigger.install availability. + setInstallTriggerPrefs(); + + Harness.installOriginBlockedCallback = install_blocked; + Harness.installsCompletedCallback = finish_test; + Harness.finalContentEvent = "InstallComplete"; + Harness.setup(); + + PermissionTestUtils.add( + "http://example.com/", + "install", + Services.perms.ALLOW_ACTION + ); + + var inner_url = encodeURIComponent( + TESTROOT + + "installtrigger.html?" + + encodeURIComponent( + JSON.stringify({ + "Unsigned XPI": { + URL: TESTROOT + "amosigned.xpi", + IconURL: TESTROOT + "icon.png", + toString() { + return this.URL; + }, + }, + }) + ) + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + TESTROOT2 + "installtrigger_frame.html?" + inner_url + ); +} + +function install_blocked(installInfo) { + wasOriginBlocked = true; +} + +function finish_test(count) { + ok( + wasOriginBlocked, + "Should have been blocked due to the cross origin request." + ); + + is(count, 0, "No add-ons should have been installed"); + PermissionTestUtils.remove("http://example.com", "install"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_url.js b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_url.js new file mode 100644 index 0000000000..ef5723640e --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_url.js @@ -0,0 +1,43 @@ +// ---------------------------------------------------------------------------- +// Tests installing an unsigned add-on by navigating directly to the url +function test() { + waitForExplicitFinish(); + SpecialPowers.pushPrefEnv( + { + set: [ + // Relax the user input requirements while running this test. + ["xpinstall.userActivation.required", false], + ], + }, + runTest + ); +} + +function runTest() { + Harness.installConfirmCallback = confirm_install; + Harness.installEndedCallback = install_ended; + Harness.installsCompletedCallback = finish_test; + Harness.setup(); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => { + BrowserTestUtils.startLoadingURIString(gBrowser, TESTROOT + "unsigned.xpi"); + }); +} + +function confirm_install(panel) { + is(panel.getAttribute("name"), "XPI Test", "Should have seen the name"); + return true; +} + +function install_ended(install, addon) { + return addon.uninstall(); +} + +function finish_test(count) { + is(count, 1, "1 Add-on should have been successfully installed"); + + gBrowser.removeCurrentTab(); + Harness.finish(); +} +// ---------------------------------------------------------------------------- diff --git a/toolkit/mozapps/extensions/test/xpinstall/bug540558.html b/toolkit/mozapps/extensions/test/xpinstall/bug540558.html new file mode 100644 index 0000000000..045e3e2d7c --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/bug540558.html @@ -0,0 +1,24 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html> + +<!-- This page tests that window.InstallTrigger.install works --> + +<head> +<title>InstallTrigger tests</title> +<script type="text/javascript"> +/* exported startInstall */ +function startInstall() { + window.InstallTrigger.install({ + "Unsigned XPI": "amosigned.xpi", + }); +} +</script> +</head> +<body onload="startInstall()"> +<p>InstallTrigger tests</p> +<p id="return"></p> +<p id="status"></p> +</body> +</html> diff --git a/toolkit/mozapps/extensions/test/xpinstall/bug638292.html b/toolkit/mozapps/extensions/test/xpinstall/bug638292.html new file mode 100644 index 0000000000..198207d4bf --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/bug638292.html @@ -0,0 +1,17 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html> + +<!-- This page tests InstallTrigger is defined in a new window --> + +<head> +<title>InstallTrigger tests</title> +</head> +<body> +<p>InstallTrigger tests</p> +<p><a id="link1" target="_blank" href="enabled.html">Open window with target</a></p> +<p><a id="link2" onclick="window.open(this.href); return false" href="enabled.html">Open window with JS</a></p> +<p><a id="link3" href="enabled.html">Open window with middle-click</a></p> +</body> +</html> diff --git a/toolkit/mozapps/extensions/test/xpinstall/bug645699.html b/toolkit/mozapps/extensions/test/xpinstall/bug645699.html new file mode 100644 index 0000000000..d993ad070d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/bug645699.html @@ -0,0 +1,32 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html> + +<head> +<title>InstallTrigger tests</title> +<script type="text/javascript"> +/* globals InstallTrigger */ +/* exported startInstall */ +function startInstall() { + var whiteUrl = "https://example.org/"; + + try { + Object.defineProperty(window, "location", { value: { href: whiteUrl } }); + throw new Error("Object.defineProperty(window, 'location', ...) should have thrown"); + } catch (exc) { + if (!(exc instanceof TypeError)) + throw exc; + } + Object.defineProperty(document, "documentURIObject", { spec: { href: whiteUrl } }); + + InstallTrigger.install({ + "Unsigned XPI": "http://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi", + }); +} +</script> +</head> +<body onload="startInstall()"> +<p>InstallTrigger tests</p> +</body> +</html> diff --git a/toolkit/mozapps/extensions/test/xpinstall/cookieRedirect.sjs b/toolkit/mozapps/extensions/test/xpinstall/cookieRedirect.sjs new file mode 100644 index 0000000000..5ddc6f9bd4 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/cookieRedirect.sjs @@ -0,0 +1,23 @@ +// Simple script redirects to the query part of the uri if the cookie "xpinstall" +// has the value "true", otherwise gives a 500 error. + +function handleRequest(request, response) { + let cookie = null; + if (request.hasHeader("Cookie")) { + let cookies = request.getHeader("Cookie").split(";"); + for (let i = 0; i < cookies.length; i++) { + if (cookies[i].substring(0, 10) == "xpinstall=") { + cookie = cookies[i].substring(10); + } + } + } + + if (cookie == "true") { + response.setStatusLine(request.httpVersion, 302, "Found"); + response.setHeader("Location", request.queryString); + response.write("See " + request.queryString); + } else { + response.setStatusLine(request.httpVersion, 500, "Internal Server Error"); + response.write("Invalid request"); + } +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi b/toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi new file mode 100644 index 0000000000..35d7bd5e5d --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi @@ -0,0 +1 @@ +This is a corrupt zip file diff --git a/toolkit/mozapps/extensions/test/xpinstall/empty.xpi b/toolkit/mozapps/extensions/test/xpinstall/empty.xpi Binary files differnew file mode 100644 index 0000000000..74ed2b8174 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/empty.xpi diff --git a/toolkit/mozapps/extensions/test/xpinstall/enabled.html b/toolkit/mozapps/extensions/test/xpinstall/enabled.html new file mode 100644 index 0000000000..dea8a59036 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/enabled.html @@ -0,0 +1,25 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html> + +<!-- This page will test if InstallTrigger seems to be enabled --> + +<head> +<title>InstallTrigger tests</title> +<script type="text/javascript"> +/* globals InstallTrigger */ +/* exported init */ +function init() { + document.getElementById("enabled").textContent = InstallTrigger.enabled() ? "true" : "false"; + dump("Sending PageLoaded\n"); + var event = new CustomEvent("PageLoaded"); + window.dispatchEvent(event); +} +</script> +</head> +<body onload="init()"> +<p>InstallTrigger tests</p> +<p id="enabled"></p> +</body> +</html> diff --git a/toolkit/mozapps/extensions/test/xpinstall/hashRedirect.sjs b/toolkit/mozapps/extensions/test/xpinstall/hashRedirect.sjs new file mode 100644 index 0000000000..8137f9e58a --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/hashRedirect.sjs @@ -0,0 +1,14 @@ +// Simple script redirects takes the query part of te request and splits it on +// the | character. Anything before is included as the X-Target-Digest header +// the latter part is used as the url to redirect to + +function handleRequest(request, response) { + let pos = request.queryString.indexOf("|"); + let header = request.queryString.substring(0, pos); + let url = request.queryString.substring(pos + 1); + + response.setStatusLine(request.httpVersion, 302, "Found"); + response.setHeader("X-Target-Digest", header); + response.setHeader("Location", url); + response.write("See " + url); +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/head.js b/toolkit/mozapps/extensions/test/xpinstall/head.js new file mode 100644 index 0000000000..33ac33c830 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/head.js @@ -0,0 +1,568 @@ +/* eslint no-unused-vars: ["error", {vars: "local", args: "none"}] */ + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +const RELATIVE_DIR = "toolkit/mozapps/extensions/test/xpinstall/"; + +const TESTROOT = "http://example.com/browser/" + RELATIVE_DIR; +const TESTROOT2 = "http://example.org/browser/" + RELATIVE_DIR; +const PROMPT_URL = "chrome://global/content/commonDialog.xhtml"; +const ADDONS_URL = "chrome://mozapps/content/extensions/aboutaddons.html"; +const PREF_LOGGING_ENABLED = "extensions.logging.enabled"; +const PREF_INSTALL_REQUIREBUILTINCERTS = + "extensions.install.requireBuiltInCerts"; +const PREF_INSTALL_REQUIRESECUREORIGIN = + "extensions.install.requireSecureOrigin"; +const CHROME_NAME = "mochikit"; + +function getChromeRoot(path) { + if (path === undefined) { + return "chrome://" + CHROME_NAME + "/content/browser/" + RELATIVE_DIR; + } + return getRootDirectory(path); +} + +function extractChromeRoot(path) { + var chromeRootPath = getChromeRoot(path); + var jar = getJar(chromeRootPath); + if (jar) { + var tmpdir = extractJarToTmp(jar); + return "file://" + tmpdir.path + "/"; + } + return chromeRootPath; +} + +function setInstallTriggerPrefs() { + Services.prefs.setBoolPref("extensions.InstallTrigger.enabled", true); + Services.prefs.setBoolPref("extensions.InstallTriggerImpl.enabled", true); + // Relax the user input requirements while running tests that call this test helper. + Services.prefs.setBoolPref("xpinstall.userActivation.required", false); + registerCleanupFunction(clearInstallTriggerPrefs); +} + +function clearInstallTriggerPrefs() { + Services.prefs.clearUserPref("extensions.InstallTrigger.enabled"); + Services.prefs.clearUserPref("extensions.InstallTriggerImpl.enabled"); + Services.prefs.clearUserPref("xpinstall.userActivation.required"); +} + +/** + * This is a test harness designed to handle responding to UI during the process + * of installing an XPI. A test can set callbacks to hear about specific parts + * of the sequence. + * Before use setup must be called and finish must be called afterwards. + */ +var Harness = { + // If set then the callback is called when an install is attempted and + // software installation is disabled. + installDisabledCallback: null, + // If set then the callback is called when an install is attempted and + // then canceled. + installCancelledCallback: null, + // If set then the callback will be called when an install's origin is blocked. + installOriginBlockedCallback: null, + // If set then the callback will be called when an install is blocked by the + // whitelist. The callback should return true to continue with the install + // anyway. + installBlockedCallback: null, + // If set will be called in the event of authentication being needed to get + // the xpi. Should return a 2 element array of username and password, or + // null to not authenticate. + authenticationCallback: null, + // If set this will be called to allow checking the contents of the xpinstall + // confirmation dialog. The callback should return true to continue the install. + installConfirmCallback: null, + // If set will be called when downloading of an item has begun. + downloadStartedCallback: null, + // If set will be called during the download of an item. + downloadProgressCallback: null, + // If set will be called when an xpi fails to download. + downloadFailedCallback: null, + // If set will be called when an xpi download is cancelled. + downloadCancelledCallback: null, + // If set will be called when downloading of an item has ended. + downloadEndedCallback: null, + // If set will be called when installation by the extension manager of an xpi + // item starts + installStartedCallback: null, + // If set will be called when an xpi fails to install. + installFailedCallback: null, + // If set will be called when each xpi item to be installed completes + // installation. + installEndedCallback: null, + // If set will be called when all triggered items are installed or the install + // is canceled. + installsCompletedCallback: null, + // If set the harness will wait for this DOM event before calling + // installsCompletedCallback + finalContentEvent: null, + + waitingForEvent: false, + pendingCount: null, + installCount: null, + runningInstalls: null, + + waitingForFinish: false, + + // A unique value to return from the installConfirmCallback to indicate that + // the install UI shouldn't be closed automatically + leaveOpen: {}, + + // Setup and tear down functions + setup(win = window) { + if (!this.waitingForFinish) { + waitForExplicitFinish(); + this.waitingForFinish = true; + + Services.prefs.setBoolPref(PREF_INSTALL_REQUIRESECUREORIGIN, false); + Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + Services.obs.addObserver(this, "addon-install-started"); + Services.obs.addObserver(this, "addon-install-disabled"); + Services.obs.addObserver(this, "addon-install-origin-blocked"); + Services.obs.addObserver(this, "addon-install-blocked"); + Services.obs.addObserver(this, "addon-install-failed"); + + // For browser_auth tests which trigger auth dialogs. + Services.obs.addObserver(this, "tabmodal-dialog-loaded"); + Services.obs.addObserver(this, "common-dialog-loaded"); + + this._boundWin = Cu.getWeakReference(win); // need this so our addon manager listener knows which window to use. + AddonManager.addInstallListener(this); + AddonManager.addAddonListener(this); + + win.addEventListener("popupshown", this); + win.PanelUI.notificationPanel.addEventListener("popupshown", this); + + var self = this; + registerCleanupFunction(async function () { + Services.prefs.clearUserPref(PREF_LOGGING_ENABLED); + Services.prefs.clearUserPref(PREF_INSTALL_REQUIRESECUREORIGIN); + Services.prefs.clearUserPref( + "network.cookieJarSettings.unblocked_for_testing" + ); + + Services.obs.removeObserver(self, "addon-install-started"); + Services.obs.removeObserver(self, "addon-install-disabled"); + Services.obs.removeObserver(self, "addon-install-origin-blocked"); + Services.obs.removeObserver(self, "addon-install-blocked"); + Services.obs.removeObserver(self, "addon-install-failed"); + + Services.obs.removeObserver(self, "tabmodal-dialog-loaded"); + Services.obs.removeObserver(self, "common-dialog-loaded"); + + AddonManager.removeInstallListener(self); + AddonManager.removeAddonListener(self); + + win.removeEventListener("popupshown", self); + win.PanelUI.notificationPanel.removeEventListener("popupshown", self); + win = null; + + let aInstalls = await AddonManager.getAllInstalls(); + is( + aInstalls.length, + 0, + "Should be no active installs at the end of the test" + ); + await Promise.all( + aInstalls.map(async function (aInstall) { + info( + "Install for " + + aInstall.sourceURI + + " is in state " + + aInstall.state + ); + if (aInstall.state == AddonManager.STATE_INSTALLED) { + await aInstall.addon.uninstall(); + } else { + aInstall.cancel(); + } + }) + ); + }); + } + + this.installCount = 0; + this.pendingCount = 0; + this.runningInstalls = []; + }, + + finish(win = window) { + // Some tests using this harness somehow finish leaving + // the addon-installed panel open. hiding here addresses + // that which fixes the rest of the tests. Since no test + // here cares about this panel, we just need it to close. + win.PanelUI.notificationPanel.hidePopup(); + win.AppMenuNotifications.removeNotification("addon-installed"); + delete this._boundWin; + finish(); + }, + + endTest() { + let callback = this.installsCompletedCallback; + let count = this.installCount; + + is(this.runningInstalls.length, 0, "Should be no running installs left"); + this.runningInstalls.forEach(function (aInstall) { + info( + "Install for " + aInstall.sourceURI + " is in state " + aInstall.state + ); + }); + + this.installOriginBlockedCallback = null; + this.installBlockedCallback = null; + this.authenticationCallback = null; + this.installConfirmCallback = null; + this.downloadStartedCallback = null; + this.downloadProgressCallback = null; + this.downloadCancelledCallback = null; + this.downloadFailedCallback = null; + this.downloadEndedCallback = null; + this.installStartedCallback = null; + this.installFailedCallback = null; + this.installEndedCallback = null; + this.installsCompletedCallback = null; + this.runningInstalls = null; + + if (callback) { + executeSoon(() => callback(count)); + } + }, + + promptReady(dialog) { + let promptType = dialog.args.promptType; + + switch (promptType) { + case "alert": + case "alertCheck": + case "confirmCheck": + case "confirm": + case "confirmEx": + PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 0 }); + break; + case "promptUserAndPass": + // This is a login dialog, hopefully an authentication prompt + // for the xpi. + if (this.authenticationCallback) { + var auth = this.authenticationCallback(); + if (auth && auth.length == 2) { + PromptTestUtils.handlePrompt(dialog, { + loginInput: auth[0], + passwordInput: auth[1], + buttonNumClick: 0, + }); + } else { + PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 1 }); + } + } else { + PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 1 }); + } + break; + default: + ok(false, "prompt type " + promptType + " not handled in test."); + break; + } + }, + + popupReady(panel) { + if (this.installBlockedCallback) { + ok(false, "Should have been blocked by the whitelist"); + } + this.pendingCount++; + + // If there is a confirm callback then its return status determines whether + // to install the items or not. If not the test is over. + let result = true; + if (this.installConfirmCallback) { + result = this.installConfirmCallback(panel); + if (result === this.leaveOpen) { + return; + } + } + + const panelEl = panel.closest("panel"); + const panelState = panelEl.state; + + const clickButton = () => { + info(`Clicking ${result ? "primary" : "secondary"} panel button`); + Assert.equal( + panelEl.state, + "open", + "Expect panel state to be open when clicking panel buttons" + ); + if (!result) { + panel.secondaryButton.click(); + } else { + panel.button.click(); + } + }; + + if (panelState === "showing") { + info( + "panel is still showing, wait for 'popup-shown' topic to be notified" + ); + BrowserUtils.promiseObserved( + "popup-shown", + shownPanel => shownPanel === panelEl + ).then(clickButton); + } else { + clickButton(); + } + }, + + handleEvent(event) { + if (event.type === "popupshown") { + if (event.target == event.view.PanelUI.notificationPanel) { + event.view.PanelUI.notificationPanel.hidePopup(); + } else if (event.target.firstElementChild) { + let popupId = event.target.firstElementChild.getAttribute("popupid"); + if (popupId === "addon-webext-permissions") { + this.popupReady(event.target.firstElementChild); + } else if (popupId === "addon-install-failed") { + event.target.firstElementChild.button.click(); + } + } + } + }, + + // Install blocked handling + + installDisabled(installInfo) { + ok( + !!this.installDisabledCallback, + "Installation shouldn't have been disabled" + ); + if (this.installDisabledCallback) { + this.installDisabledCallback(installInfo); + } + this.expectingCancelled = true; + this.expectingCancelled = false; + this.endTest(); + }, + + installCancelled(installInfo) { + if (this.expectingCancelled) { + return; + } + + ok( + !!this.installCancelledCallback, + "Installation shouldn't have been cancelled" + ); + if (this.installCancelledCallback) { + this.installCancelledCallback(installInfo); + } + this.endTest(); + }, + + installOriginBlocked(installInfo) { + ok(!!this.installOriginBlockedCallback, "Shouldn't have been blocked"); + if (this.installOriginBlockedCallback) { + this.installOriginBlockedCallback(installInfo); + } + this.endTest(); + }, + + installBlocked(installInfo) { + ok( + !!this.installBlockedCallback, + "Shouldn't have been blocked by the whitelist" + ); + if ( + this.installBlockedCallback && + this.installBlockedCallback(installInfo) + ) { + this.installBlockedCallback = null; + installInfo.install(); + } else { + this.expectingCancelled = true; + installInfo.installs.forEach(function (install) { + install.cancel(); + }); + this.expectingCancelled = false; + this.endTest(); + } + }, + + // Addon Install Listener + + onNewInstall(install) { + this.runningInstalls.push(install); + + if (this.finalContentEvent && !this.waitingForEvent) { + this.waitingForEvent = true; + info("Waiting for " + this.finalContentEvent); + BrowserTestUtils.waitForContentEvent( + this._boundWin.get().gBrowser.selectedBrowser, + this.finalContentEvent, + true, + null, + true + ).then(() => { + info("Saw " + this.finalContentEvent + "," + this.waitingForEvent); + this.waitingForEvent = false; + if (this.pendingCount == 0) { + this.endTest(); + } + }); + } + }, + + onDownloadStarted(install) { + this.pendingCount++; + if (this.downloadStartedCallback) { + this.downloadStartedCallback(install); + } + }, + + onDownloadProgress(install) { + if (this.downloadProgressCallback) { + this.downloadProgressCallback(install); + } + }, + + onDownloadEnded(install) { + if (this.downloadEndedCallback) { + this.downloadEndedCallback(install); + } + }, + + onDownloadCancelled(install) { + isnot( + this.runningInstalls.indexOf(install), + -1, + "Should only see cancelations for started installs" + ); + this.runningInstalls.splice(this.runningInstalls.indexOf(install), 1); + + if ( + this.downloadCancelledCallback && + this.downloadCancelledCallback(install) === false + ) { + return; + } + this.checkTestEnded(); + }, + + onDownloadFailed(install) { + if (this.downloadFailedCallback) { + this.downloadFailedCallback(install); + } + this.checkTestEnded(); + }, + + onInstallStarted(install) { + if (this.installStartedCallback) { + this.installStartedCallback(install); + } + }, + + async onInstallEnded(install, addon) { + this.installCount++; + if (this.installEndedCallback) { + await this.installEndedCallback(install, addon); + } + this.checkTestEnded(); + }, + + onInstallFailed(install) { + if (this.installFailedCallback) { + this.installFailedCallback(install); + } + this.checkTestEnded(); + }, + + onUninstalled(addon) { + let idx = this.runningInstalls.findIndex(install => install.addon == addon); + if (idx != -1) { + this.runningInstalls.splice(idx, 1); + this.checkTestEnded(); + } + }, + + onInstallCancelled(install) { + // This is ugly. We have a bunch of tests that cancel installs + // but don't expect this event to be raised. + // For at least one test (browser_whitelist3.js), we used to generate + // onDownloadCancelled when the user cancelled the installation at the + // confirmation prompt. We're now generating onInstallCancelled instead + // of onDownloadCancelled but making this code unconditional breaks a + // bunch of other tests. Ugh. + let idx = this.runningInstalls.indexOf(install); + if (idx != -1) { + this.runningInstalls.splice(this.runningInstalls.indexOf(install), 1); + this.checkTestEnded(); + } + }, + + checkTestEnded() { + if (--this.pendingCount == 0 && !this.waitingForEvent) { + this.endTest(); + } + }, + + // nsIObserver + + observe(subject, topic, data) { + var installInfo = subject.wrappedJSObject; + switch (topic) { + case "addon-install-started": + is( + this.runningInstalls.length, + installInfo.installs.length, + "Should have seen the expected number of installs started" + ); + break; + case "addon-install-disabled": + this.installDisabled(installInfo); + break; + case "addon-install-cancelled": + this.installCancelled(installInfo); + break; + case "addon-install-origin-blocked": + this.installOriginBlocked(installInfo); + break; + case "addon-install-blocked": + this.installBlocked(installInfo); + break; + case "addon-install-failed": + installInfo.installs.forEach(function (aInstall) { + isnot( + this.runningInstalls.indexOf(aInstall), + -1, + "Should only see failures for started installs" + ); + + ok( + aInstall.error != 0 || aInstall.addon.appDisabled, + "Failed installs should have an error or be appDisabled" + ); + + this.runningInstalls.splice( + this.runningInstalls.indexOf(aInstall), + 1 + ); + }, this); + break; + case "tabmodal-dialog-loaded": + let browser = subject.ownerGlobal.gBrowser.selectedBrowser; + let prompt = browser.tabModalPromptBox.getPrompt(subject); + this.promptReady(prompt.Dialog); + break; + case "common-dialog-loaded": + this.promptReady(subject.Dialog); + break; + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), +}; diff --git a/toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi b/toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi Binary files differnew file mode 100644 index 0000000000..de895fd1d9 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi diff --git a/toolkit/mozapps/extensions/test/xpinstall/installchrome.html b/toolkit/mozapps/extensions/test/xpinstall/installchrome.html new file mode 100644 index 0000000000..d9ff573ab5 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/installchrome.html @@ -0,0 +1,23 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html> + +<!-- This page will accept a url as the uri query and pass it to InstallTrigger.installChrome --> + +<head> +<title>InstallTrigger tests</title> +<script type="text/javascript"> +/* globals InstallTrigger */ +/* exported startInstall */ +function startInstall() { + InstallTrigger.installChrome(InstallTrigger.SKIN, + decodeURIComponent(document.location.search.substring(1)), + "test"); +} +</script> +</head> +<body onload="startInstall()"> +<p>InstallTrigger tests</p> +</body> +</html> diff --git a/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html b/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html new file mode 100644 index 0000000000..d68e4acbe3 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html @@ -0,0 +1,57 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html> + +<!-- This page will accept some json as the uri query and pass it to InstallTrigger.install --> + +<head> +<title>InstallTrigger tests</title> +<script type="text/javascript"> +/* globals InstallTrigger */ +/* exported startInstall */ +function installCallback(url, status) { + document.getElementById("status").textContent = status; + + dump("Sending InstallComplete\n"); + var event = new CustomEvent("InstallComplete"); + var target = window.parent ? window.parent : window; + target.dispatchEvent(event); +} + +function startInstall(viaWindowLoaded = false) { + var event = new CustomEvent("InstallTriggered"); + var text; + if (viaWindowLoaded) { + text = decodeURIComponent(document.location.search.substring(1)); + } else { + text = decodeURIComponent(document.location.search.substring("?manualStartInstall".length)); + } + var triggers = JSON.parse(text); + try { + document.getElementById("return").textContent = InstallTrigger.install(triggers, installCallback); + dump("Sending InstallTriggered\n"); + window.dispatchEvent(event); + } catch (e) { + document.getElementById("return").textContent = "exception"; + dump("Sending InstallTriggered\n"); + window.dispatchEvent(event); + if (viaWindowLoaded) { + throw e; + } + } +} + +window.onload = function () { + if (!document.location.search.startsWith("?manualStartInstall")) { + startInstall(true); + } +} +</script> +</head> +<body> +<p>InstallTrigger tests</p> +<p id="return"></p> +<p id="status"></p> +</body> +</html> diff --git a/toolkit/mozapps/extensions/test/xpinstall/installtrigger_frame.html b/toolkit/mozapps/extensions/test/xpinstall/installtrigger_frame.html new file mode 100644 index 0000000000..7e4bccab18 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/installtrigger_frame.html @@ -0,0 +1,30 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html> + +<!-- This page will accept some url as the uri query and load it in + an inner iframe, which will run InstallTrigger.install --> + +<head> +<title>InstallTrigger frame tests</title> +<script type="text/javascript"> +/* exported prepChild */ +function prepChild() { + // Pass our parameters over to the child + var child = window.frames[0]; + var url = decodeURIComponent(document.location.search.substr(1)); + child.location = url; +} +</script> +</head> +<body onload="prepChild()"> + +<iframe src="about:blank"> +</iframe> + +<p>InstallTrigger tests</p> +<p id="return"></p> +<p id="status"></p> +</body> +</html> diff --git a/toolkit/mozapps/extensions/test/xpinstall/navigate.html b/toolkit/mozapps/extensions/test/xpinstall/navigate.html new file mode 100644 index 0000000000..96052009b9 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/navigate.html @@ -0,0 +1,25 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html> + +<!-- This page will accept some url as the uri query and navigate to it by + clicking a link --> + +<head> +<title>Navigation tests</title> +<script type="text/javascript"> +/* exported navigate */ +function navigate() { + var url = decodeURIComponent(document.location.search.substr(1)); + var link = document.getElementById("link"); + link.href = url; + link.click(); +} +</script> +</head> +<body onload="navigate()"> + +<p><a id="link">Test Link</a></p> +</body> +</html> diff --git a/toolkit/mozapps/extensions/test/xpinstall/recommended.xpi b/toolkit/mozapps/extensions/test/xpinstall/recommended.xpi Binary files differnew file mode 100644 index 0000000000..e180decfc5 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/recommended.xpi diff --git a/toolkit/mozapps/extensions/test/xpinstall/redirect.sjs b/toolkit/mozapps/extensions/test/xpinstall/redirect.sjs new file mode 100644 index 0000000000..14236ee821 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/redirect.sjs @@ -0,0 +1,39 @@ +// Script has two modes based on the query string. If the mode is "setup" then +// parameters from the query string configure the redirection. If the mode is +// "redirect" then a redirect is returned + +function handleRequest(request, response) { + let parts = request.queryString.split("&"); + let settings = {}; + + parts.forEach(function (aString) { + let [k, v] = aString.split("="); + settings[k] = decodeURIComponent(v); + }); + + if (settings.mode == "setup") { + delete settings.mode; + + // Object states must be an nsISupports + var state = { + settings, + QueryInterface: ChromeUtils.generateQI([]), + }; + state.wrappedJSObject = state; + + setObjectState("xpinstall-redirect-settings", state); + response.setStatusLine(request.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/plain"); + response.write("Setup complete"); + } else if (settings.mode == "redirect") { + getObjectState("xpinstall-redirect-settings", function (aObject) { + settings = aObject.wrappedJSObject.settings; + }); + + response.setStatusLine(request.httpVersion, 302, "Found"); + for (var name in settings) { + response.setHeader(name, settings[name]); + } + response.write("Done"); + } +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi b/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi Binary files differnew file mode 100644 index 0000000000..9fee8f60b1 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi diff --git a/toolkit/mozapps/extensions/test/xpinstall/slowinstall.sjs b/toolkit/mozapps/extensions/test/xpinstall/slowinstall.sjs new file mode 100644 index 0000000000..e2a889c329 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/slowinstall.sjs @@ -0,0 +1,103 @@ +// In an SJS file we need to get NetUtil ourselves, despite +// what eslint might think applies for browser tests. +// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix +let { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); + +const RELATIVE_PATH = "browser/toolkit/mozapps/extensions/test/xpinstall"; +const NOTIFICATION_TOPIC = "slowinstall-complete"; + +/** + * Helper function to create a JS object representing the url parameters from + * the request's queryString. + * + * @param aQueryString + * The request's query string. + * @return A JS object representing the url parameters from the request's + * queryString. + */ +function parseQueryString(aQueryString) { + var paramArray = aQueryString.split("&"); + var regex = /^([^=]+)=(.*)$/; + var params = {}; + for (var i = 0, sz = paramArray.length; i < sz; i++) { + var match = regex.exec(paramArray[i]); + if (!match) { + throw new Error("Bad parameter in queryString! '" + paramArray[i] + "'"); + } + params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]); + } + + return params; +} + +function handleRequest(aRequest, aResponse) { + let id = +getState("ID"); + setState("ID", "" + (id + 1)); + + function LOG(str) { + dump("slowinstall.sjs[" + id + "]: " + str + "\n"); + } + + aResponse.setStatusLine(aRequest.httpVersion, 200, "OK"); + + var params = {}; + if (aRequest.queryString) { + params = parseQueryString(aRequest.queryString); + } + + if (params.file) { + let xpiFile = ""; + + function complete_download() { + LOG("Completing download"); + + try { + // Doesn't seem to be a sane way to read using IOUtils and write to an + // nsIOutputStream so here we are. + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(xpiFile); + let stream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + stream.init(file, -1, -1, stream.DEFER_OPEN + stream.CLOSE_ON_EOF); + + NetUtil.asyncCopy(stream, aResponse.bodyOutputStream, () => { + LOG("Download complete"); + aResponse.finish(); + }); + } catch (e) { + LOG("Exception " + e); + } + } + + let waitForComplete = new Promise(resolve => { + function complete() { + Services.obs.removeObserver(complete, NOTIFICATION_TOPIC); + resolve(); + } + + Services.obs.addObserver(complete, NOTIFICATION_TOPIC); + }); + + aResponse.processAsync(); + + const dir = Services.dirsvc.get("CurWorkD", Ci.nsIFile).path; + xpiFile = PathUtils.join(dir, ...RELATIVE_PATH.split("/"), params.file); + LOG("Starting slow download of " + xpiFile); + + IOUtils.stat(xpiFile).then(info => { + aResponse.setHeader("Content-Type", "binary/octet-stream"); + aResponse.setHeader("Content-Length", info.size.toString()); + + LOG("Download paused"); + waitForComplete.then(complete_download); + }); + } else if (params.continue) { + dump( + "slowinstall.sjs: Received signal to complete all current downloads.\n" + ); + Services.obs.notifyObservers(null, NOTIFICATION_TOPIC); + } +} diff --git a/toolkit/mozapps/extensions/test/xpinstall/startsoftwareupdate.html b/toolkit/mozapps/extensions/test/xpinstall/startsoftwareupdate.html new file mode 100644 index 0000000000..83792ebdb2 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/startsoftwareupdate.html @@ -0,0 +1,21 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html> + +<!-- This page will accept a url as the uri query and pass it to InstallTrigger.startSoftwareUpdate --> + +<head> +<title>InstallTrigger tests</title> +<script type="text/javascript"> +/* globals InstallTrigger */ +/* exported startInstall */ +function startInstall() { + InstallTrigger.startSoftwareUpdate(decodeURIComponent(document.location.search.substring(1))); +} +</script> +</head> +<body onload="startInstall()"> +<p>InstallTrigger tests</p> +</body> +</html> diff --git a/toolkit/mozapps/extensions/test/xpinstall/triggerredirect.html b/toolkit/mozapps/extensions/test/xpinstall/triggerredirect.html new file mode 100644 index 0000000000..1b098d6948 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/triggerredirect.html @@ -0,0 +1,37 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html> + +<!-- This page will attempt an install and then try to load a new page in the tab --> + +<head> +<title>InstallTrigger tests</title> +<script type="text/javascript"> +/* globals InstallTrigger */ +/* exported startInstall */ +function installCallback(url, status) { + document.location = "#foo"; + + dump("Sending InstallComplete\n"); + var event = new CustomEvent("InstallComplete"); + window.dispatchEvent(event); +} + +function startInstall() { + InstallTrigger.install({ + "Unsigned XPI": { + URL: "amosigned.xpi", + IconURL: "icon.png", + toString() { return this.URL; }, + }, + }, installCallback); +} +</script> +</head> +<body onload="startInstall()"> +<p>InstallTrigger tests</p> +<p id="return"></p> +<p id="status"></p> +</body> +</html> diff --git a/toolkit/mozapps/extensions/test/xpinstall/unsigned.xpi b/toolkit/mozapps/extensions/test/xpinstall/unsigned.xpi Binary files differnew file mode 100644 index 0000000000..95f99a748f --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/unsigned.xpi diff --git a/toolkit/mozapps/extensions/test/xpinstall/unsigned_mv3.xpi b/toolkit/mozapps/extensions/test/xpinstall/unsigned_mv3.xpi Binary files differnew file mode 100644 index 0000000000..7ef5534f45 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/unsigned_mv3.xpi diff --git a/toolkit/mozapps/extensions/test/xpinstall/webmidi_permission.xpi b/toolkit/mozapps/extensions/test/xpinstall/webmidi_permission.xpi Binary files differnew file mode 100644 index 0000000000..9a2effdd0f --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/webmidi_permission.xpi |