summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/extensions/test
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/extensions/test')
-rw-r--r--toolkit/mozapps/extensions/test/browser/.eslintrc.js14
-rw-r--r--toolkit/mozapps/extensions/test/browser/addon_prefs.xhtml6
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/manifest.mf8
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.rsabin0 -> 4210 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.sf5
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/manifest.json12
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/manifest.mf8
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.rsabin0 -> 4210 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.sf5
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/manifest.json12
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/manifest.mf8
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.rsabin0 -> 4218 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.sf5
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/manifest.json13
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/manifest.mf8
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.rsabin0 -> 4213 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.sf5
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_installssl/manifest.json12
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_theme/manifest.json22
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/manifest.mf12
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.rsabin0 -> 4197 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.sf4
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/options_signed/manifest.json11
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/options_signed/options.html9
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser.toml193
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_AMBrowserExtensionsImport.js220
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_about_debugging_link.js129
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_addon_list_reordering.js204
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_amo_abuse_report.js85
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug572561.js96
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_checkAddonCompatibility.js36
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_colorwaybuiltins_migration.js265
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_dragdrop.js270
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_file_xpi_no_process_switch.js122
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_globalwarnings.js176
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js406
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_history_navigation.js623
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js1093
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_abuse_report_dialog.js185
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_detail_permissions.js827
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js1675
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js668
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_discover_view_clientid.js219
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_discover_view_prefs.js83
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_list_view.js1063
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_list_view_recommendations.js293
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_message_bar.js185
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_options_ui.js651
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_options_ui_in_tab.js136
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_pending_updates.js311
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_recent_updates.js180
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_recommendations.js165
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_scroll_restoration.js229
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_sitepermission_addons.js178
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_updates.js750
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_warning_messages.js290
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_installssl.js378
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_installtrigger_install.js362
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_local_install.js245
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts.js331
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_hidden.js198
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_remove.js180
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_menu_button_accessibility.js93
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_page_accessibility.js15
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_page_options_install_addon.js128
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_page_options_updates.js124
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_permission_prompt.js178
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_reinstall.js277
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_shortcuts_duplicate_check.js262
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_sidebar_categories.js166
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_sidebar_hidden_categories.js214
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_sidebar_restore_category.js76
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_subframe_install.js234
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_task_next_test.js17
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_updateid.js87
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_updatessl.js389
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_updatessl.json17
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_updatessl.json^headers^1
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_verify_l10n_strings.js62
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi.js125
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_abuse_report.js375
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_access.js146
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js124
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_enable.js63
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_install.js652
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_install_disabled.js60
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_theme.js79
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_uninstall.js72
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webext_icon.js82
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webext_incognito.js593
-rw-r--r--toolkit/mozapps/extensions/test/browser/discovery/api_response.json679
-rw-r--r--toolkit/mozapps/extensions/test/browser/discovery/api_response_empty.json1
-rw-r--r--toolkit/mozapps/extensions/test/browser/discovery/small-1x1.pngbin0 -> 82 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/head.js1714
-rw-r--r--toolkit/mozapps/extensions/test/browser/head_abuse_report.js615
-rw-r--r--toolkit/mozapps/extensions/test/browser/head_disco.js125
-rw-r--r--toolkit/mozapps/extensions/test/browser/moz.build31
-rw-r--r--toolkit/mozapps/extensions/test/browser/redirect.sjs5
-rw-r--r--toolkit/mozapps/extensions/test/browser/sandboxed.html11
-rw-r--r--toolkit/mozapps/extensions/test/browser/sandboxed.html^headers^1
-rw-r--r--toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html30
-rw-r--r--toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html13
-rw-r--r--toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xhtml6
-rw-r--r--toolkit/mozapps/extensions/test/browser/webapi_checkframed.html7
-rw-r--r--toolkit/mozapps/extensions/test/browser/webapi_checknavigatedwindow.html29
-rw-r--r--toolkit/mozapps/extensions/test/create_xpi.py21
-rw-r--r--toolkit/mozapps/extensions/test/mochitest/chrome.toml3
-rw-r--r--toolkit/mozapps/extensions/test/mochitest/file_empty.html2
-rw-r--r--toolkit/mozapps/extensions/test/mochitest/mochitest.toml6
-rw-r--r--toolkit/mozapps/extensions/test/mochitest/test_blocklist_gfx_initialized.html31
-rw-r--r--toolkit/mozapps/extensions/test/mochitest/test_bug887098.html70
-rw-r--r--toolkit/mozapps/extensions/test/mochitest/test_default_theme.html37
-rw-r--r--toolkit/mozapps/extensions/test/moz.build20
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/.eslintrc.js24
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/bug455906_block.xml18
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/bug455906_empty.xml7
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/bug455906_start.xml30
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/bug455906_warn.xml33
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/corrupt.xpi1
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/corruptfile.xpibin0 -> 633 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/empty.xpibin0 -> 197 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/mlbf-blocked1-unblocked2.binbin0 -> 32 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/pluginInfoURL_block.xml45
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.txt1
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.xml3
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad2.xml3
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_aus_ee.pem15
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_aus_ee.pem.certspec5
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_int.pem18
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_int.pem.certspec4
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/empty.xml5
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/good.xml11
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/missing.xml3
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/unsigned.xpibin0 -> 452 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/langpack_signed.xpibin0 -> 4452 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/langpack_unsigned.xpibin0 -> 413 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long.xpibin0 -> 4761 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/privileged.xpibin0 -> 4659 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed1.xpibin0 -> 4702 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed2.xpibin0 -> 4697 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned.xpibin0 -> 528 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_cache.json134
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_empty.json7
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_fail.json1
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getAddonsByIDs.json117
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getMappedAddons.json25
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getMappedAddons_empty.json8
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_backgroundupdate.json46
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_metadata_filters_1.xml21
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_prefs_1.xml28
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug393285.xml30
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app-extensions.json332
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app-plugins.json332
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app.xml333
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit-extensions.json189
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit-plugins.json189
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit.xml208
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug468528.xml15
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_1.xml17
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_2.xml10
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_empty.xml4
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_1.xml13
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_2.xml13
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug655254.json17
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_corrupt.json30
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete.json12
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete_legacy.json18
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer.json12
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer_legacy.json18
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore.json12
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore_legacy.json18
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_staged.json32
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist.json377
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_AllOS.json581
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_OSVersion.json20
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_install_addons.json31
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_install_compat.json27
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_no_update.json7
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/ancient.xml8
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/new.xml8
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/old.xml8
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtp.xml26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtpUndo.xml10
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_softblocked1.xml9
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_trash_directory.worker.js40
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_update.json120
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_update_addons.json14
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_update_compat.json28
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.json269
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/unsigned.xpibin0 -> 463 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/webext-implicit-id.xpibin0 -> 4182 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/head_addons.js1223
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/head_amremotesettings.js31
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/head_cert_handling.js33
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/head_compat.js47
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/head_sideload.js76
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/head_system_addons.js486
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/head_unpack.js3
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/head.js57
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_android_blocklist_dump.js84
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_addonBlockURL.js56
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_appversion.js293
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_clients.js225
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_gfx.js113
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_metadata_filters.js116
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf.js290
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_dump.js155
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_fetch.js231
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_stashes.js219
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_telemetry.js188
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_update.js75
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_osabi.js286
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_prefs.js106
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_regexp_split.js225
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_severities.js504
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_statechange_telemetry.js411
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_targetapp_filter.js392
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_telemetry.js138
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklistchange.js1389
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklistchange_v2.js13
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Device.js73
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_DriverNew.js67
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverNew.js112
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverOld.js68
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_OK.js68
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_DriverOld.js68
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_OK.js70
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_No_Comparison.js69
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OK.js69
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OS.js68
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_match.js70
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_DriverVersion.js70
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_OSVersion.js71
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Vendor.js68
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Version.js190
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_prefs.js124
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_softblocked.js61
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/xpcshell.toml102
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_AMBrowserExtensionsImport.js500
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_AbuseReporter.js908
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js488
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_appIsShuttingDown.js82
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js728
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache_locale.js217
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_langpacks.js135
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_paging.js91
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_ProductAddonChecker.js310
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_ProductAddonChecker_signatures.js201
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_QuarantinedDomains_AMRemoteSettings.js217
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_QuarantinedDomains_AddonWrapper.js207
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js133
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_XPIcancel.js70
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_addonStartup.js93
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_addon_manager_telemetry_events.js1049
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_amo_stats_telemetry.js102
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_aom_startup.js189
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bad_json.js41
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_badschema.js237
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug587088.js194
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_builtin_location.js149
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_cacheflush.js86
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_childprocess.js25
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_colorways_builtin_theme_upgrades.js582
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_cookies.js102
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_corrupt.js216
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_crash_annotation_quoting.js25
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_db_path.js64
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js556
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_dependencies.js140
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_dictionary_webextension.js263
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_distribution.js115
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_distribution_langpack.js112
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_embedderDisabled.js124
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_error.js75
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js223
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_filepointer.js327
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_general.js49
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_getInstallSourceFromHost.js47
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_gmpProvider.js477
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_harness.js13
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_hidden.js251
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_install.js1063
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_installOrigins.js549
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_install_cancel.js92
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_install_file_change.js180
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_install_icons.js62
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_installtrigger_deprecation.js346
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_installtrigger_schemes.js75
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_isDebuggable.js21
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_isReady.js71
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_loadManifest_isPrivileged.js233
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_locale.js103
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_moved_extension_metadata.js186
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_no_addons.js83
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_nodisable_hidden.js100
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_onPropertyChanged_appDisabled.js52
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_permissions.js199
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_permissions_prefs.js99
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_pref_properties.js221
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_provider_markSafe.js43
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_provider_shutdown.js96
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_shutdown.js65
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_startup.js59
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_proxies.js235
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_recommendations.js707
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_registerchrome.js88
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_registry.js160
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_reinstall_disabled_addon.js213
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_reload.js188
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_remote_pref_telemetry.js48
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_safemode.js90
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_schema_change.js157
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_seen.js277
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js131
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_shutdown_barriers.js215
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_shutdown_early.js62
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_sideload_scopes.js188
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_sideloads.js117
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_sideloads_after_rebuild.js149
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_signed_inject.js429
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_signed_install.js337
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_signed_langpack.js67
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_signed_long.js23
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_signed_updatepref.js130
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js109
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_sitePermsAddonProvider.js967
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_startup.js648
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_startup_enable.js47
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_startup_isPrivileged.js58
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_startup_scan.js125
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_strictcompatibility.js156
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_syncGUID.js113
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_allowed.js55
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_delay_update.js486
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_profile_location.js204
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_repository.js69
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js539
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_update_blank.js118
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_update_checkSizeHash.js182
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_update_custom.js492
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_update_empty.js142
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_update_enterprisepolicy.js78
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_update_fail.js186
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_update_installTelemetryInfo.js95
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_update_newset.js166
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_update_overlapping.js181
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_update_uninstall_check.js57
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_update_upgrades.js166
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_upgrades.js417
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_systemaddomstartupprefs.js56
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_temporary.js765
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_trash_directory.js47
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_types.js117
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_undouninstall.js584
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_update.js834
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_updateCancel.js139
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_update_addontype.js75
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_update_compatmode.js112
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_update_ignorecompat.js116
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_update_isPrivileged.js181
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_update_noSystemAddonUpdate.js43
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_update_strictcompat.js216
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_update_theme.js121
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_update_webextensions.js209
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_updatecheck.js167
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_updatecheck_errors.js52
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_updatecheck_json.js423
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_updateid.js82
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_updateversion.js101
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js199
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_upgrade_incompatible.js73
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_webextension.js676
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_webextension_events.js94
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js212
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js696
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_webextension_install_syntax_error.js42
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_webextension_langpack.js669
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_webextension_paths.js47
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_webextension_theme.js365
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/xpcshell-unpack.toml15
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/xpcshell.toml362
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/amosigned.xpibin0 -> 4287 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/authRedirect.sjs21
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser.toml175
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger.js86
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger_iframe.js77
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_url.js63
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_auth.js68
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_auth2.js73
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_auth3.js72
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_auth4.js71
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_badargs.js49
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_badargs2.js55
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_badhash.js46
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_badhashtype.js46
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_block_fullscreen_prompt.js129
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_bug540558.js31
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_bug611242.js34
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_bug638292.js51
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js69
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_bug645699_postDownload.js55
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_bug672485.js63
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_containers.js116
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_cookies.js42
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_cookies2.js64
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_cookies3.js68
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_cookies4.js68
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_corrupt.js53
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_datauri.js80
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js1545
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_empty.js39
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_enabled.js103
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_hash.js47
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_hash2.js47
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_httphash.js55
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_httphash2.js52
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_httphash3.js52
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_httphash4.js49
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_httphash5.js53
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_httphash6.js107
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_installchrome.js36
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_localfile.js42
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_localfile2.js61
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js42
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js55
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_localfile4_postDownload.js54
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_newwindow.js89
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_offline.js82
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_privatebrowsing.js133
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_relative.js67
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_required_useractivation.js156
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_signed_url.js32
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_softwareupdate.js36
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_trigger_redirect.js48
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger.js68
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_iframe.js77
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_xorigin.js58
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_url.js43
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/bug540558.html24
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/bug638292.html17
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/bug645699.html32
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/cookieRedirect.sjs23
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi1
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/empty.xpibin0 -> 197 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/enabled.html25
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/hashRedirect.sjs14
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/head.js568
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/incompatible.xpibin0 -> 428 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/installchrome.html23
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/installtrigger.html57
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/installtrigger_frame.html30
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/navigate.html25
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/recommended.xpibin0 -> 7884 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/redirect.sjs39
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/restartless.xpibin0 -> 4447 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/slowinstall.sjs103
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/startsoftwareupdate.html21
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/triggerredirect.html37
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/unsigned.xpibin0 -> 312 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/unsigned_mv3.xpibin0 -> 316 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/webmidi_permission.xpibin0 -> 7533 bytes
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
new file mode 100644
index 0000000000..046a0285c7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.rsa
Binary files differ
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
new file mode 100644
index 0000000000..170a361620
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.rsa
Binary files differ
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
new file mode 100644
index 0000000000..a026680e91
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.rsa
Binary files differ
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
new file mode 100644
index 0000000000..68621e19be
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.rsa
Binary files differ
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
new file mode 100644
index 0000000000..8b6320adda
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.rsa
Binary files differ
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
new file mode 100644
index 0000000000..862d1dd10c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/discovery/small-1x1.png
Binary files differ
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
new file mode 100644
index 0000000000..0c30989aa5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/corruptfile.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/empty.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/empty.xpi
new file mode 100644
index 0000000000..74ed2b8174
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/empty.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/mlbf-blocked1-unblocked2.bin b/toolkit/mozapps/extensions/test/xpcshell/data/mlbf-blocked1-unblocked2.bin
new file mode 100644
index 0000000000..fe8e08fa68
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/mlbf-blocked1-unblocked2.bin
Binary files differ
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
new file mode 100644
index 0000000000..51b00475a9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/unsigned.xpi
Binary files differ
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
new file mode 100644
index 0000000000..f60d00348e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/langpack_signed.xpi
Binary files differ
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
new file mode 100644
index 0000000000..89de7f4409
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/langpack_unsigned.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long.xpi
new file mode 100644
index 0000000000..f95f3df91e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/privileged.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/privileged.xpi
new file mode 100644
index 0000000000..c22acaacd2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/privileged.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed1.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed1.xpi
new file mode 100644
index 0000000000..e2ba7d6fd8
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed2.xpi
new file mode 100644
index 0000000000..ccb20796f2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned.xpi
new file mode 100644
index 0000000000..9e10be5db3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned.xpi
Binary files differ
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 &amp; 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
new file mode 100644
index 0000000000..12a13f139b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/unsigned.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/webext-implicit-id.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/webext-implicit-id.xpi
new file mode 100644
index 0000000000..6b4abaa691
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/webext-implicit-id.xpi
Binary files differ
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
new file mode 100644
index 0000000000..f2948e6994
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi
Binary files differ
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
new file mode 100644
index 0000000000..74ed2b8174
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/empty.xpi
Binary files differ
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
new file mode 100644
index 0000000000..de895fd1d9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi
Binary files differ
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
new file mode 100644
index 0000000000..e180decfc5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/recommended.xpi
Binary files differ
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
new file mode 100644
index 0000000000..9fee8f60b1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi
Binary files differ
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
new file mode 100644
index 0000000000..95f99a748f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/unsigned.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpinstall/unsigned_mv3.xpi b/toolkit/mozapps/extensions/test/xpinstall/unsigned_mv3.xpi
new file mode 100644
index 0000000000..7ef5534f45
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/unsigned_mv3.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpinstall/webmidi_permission.xpi b/toolkit/mozapps/extensions/test/xpinstall/webmidi_permission.xpi
new file mode 100644
index 0000000000..9a2effdd0f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/webmidi_permission.xpi
Binary files differ