summaryrefslogtreecommitdiffstats
path: root/toolkit/components/search/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/search/tests
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/search/tests')
-rw-r--r--toolkit/components/search/tests/SearchTestUtils.sys.mjs505
-rw-r--r--toolkit/components/search/tests/xpcshell/data/bigIcon.icobin0 -> 56646 bytes
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-app/manifest.json24
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-chromeicon/manifest.json23
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-diff-name/_locales/en/messages.json8
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-diff-name/_locales/gd/messages.json8
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-diff-name/manifest.json21
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-fr.xml12
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-fr/manifest.json32
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-override/manifest.json24
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-pref/manifest.json28
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-purposes/manifest.json62
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-purpose/manifest.json41
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-reordered/manifest.json40
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-resourceicon/_locales/en/messages.json8
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-resourceicon/_locales/gd/messages.json8
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-resourceicon/manifest.json21
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-same-name/_locales/en/messages.json8
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-same-name/_locales/gd/messages.json8
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-same-name/manifest.json21
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-system-purpose/manifest.json35
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine.xml15
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine/manifest.json40
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine2.xml9
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine2/manifest.json18
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engineImages.xml22
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engineImages/manifest.json37
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engineMaker.sjs77
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engines-no-order-hint.json83
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engines.json98
-rw-r--r--toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/_locales/af/messages.json17
-rw-r--r--toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/_locales/an/messages.json17
-rw-r--r--toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/favicon.icobin0 -> 884 bytes
-rw-r--r--toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/manifest.json23
-rw-r--r--toolkit/components/search/tests/xpcshell/data/iconsRedirect.sjs18
-rw-r--r--toolkit/components/search/tests/xpcshell/data/remoteIcon.icobin0 -> 901 bytes
-rw-r--r--toolkit/components/search/tests/xpcshell/data/search-legacy-correct-default-engine-hashes.json112
-rw-r--r--toolkit/components/search/tests/xpcshell/data/search-legacy-no-ids.json87
-rw-r--r--toolkit/components/search/tests/xpcshell/data/search-legacy-old-loadPaths.json135
-rw-r--r--toolkit/components/search/tests/xpcshell/data/search-legacy-wrong-default-engine-hashes.json114
-rw-r--r--toolkit/components/search/tests/xpcshell/data/search-legacy-wrong-third-party-engine-hashes.json114
-rw-r--r--toolkit/components/search/tests/xpcshell/data/search-legacy.json109
-rw-r--r--toolkit/components/search/tests/xpcshell/data/search-migration.json68
-rw-r--r--toolkit/components/search/tests/xpcshell/data/search-obsolete-app.json41
-rw-r--r--toolkit/components/search/tests/xpcshell/data/search-obsolete-distribution.json41
-rw-r--r--toolkit/components/search/tests/xpcshell/data/search-obsolete-langpack.json91
-rw-r--r--toolkit/components/search/tests/xpcshell/data/search.json72
-rw-r--r--toolkit/components/search/tests/xpcshell/data/searchSuggestions.sjs181
-rw-r--r--toolkit/components/search/tests/xpcshell/data/search_ignorelist.json51
-rw-r--r--toolkit/components/search/tests/xpcshell/data/svgIcon.svg4
-rw-r--r--toolkit/components/search/tests/xpcshell/data1/engine1/manifest.json27
-rw-r--r--toolkit/components/search/tests/xpcshell/data1/engine2/manifest.json27
-rw-r--r--toolkit/components/search/tests/xpcshell/data1/engines.json60
-rw-r--r--toolkit/components/search/tests/xpcshell/data1/exp2/manifest.json27
-rw-r--r--toolkit/components/search/tests/xpcshell/data1/exp3/manifest.json27
-rw-r--r--toolkit/components/search/tests/xpcshell/head_search.js512
-rw-r--r--toolkit/components/search/tests/xpcshell/method-extensions/engines.json45
-rw-r--r--toolkit/components/search/tests/xpcshell/method-extensions/get/manifest.json21
-rw-r--r--toolkit/components/search/tests/xpcshell/method-extensions/post/manifest.json21
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/chromeicon.xml9
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/insecure-and-insecurely-updated1.xml14
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/insecure-and-insecurely-updated2.xml14
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/insecure-and-no-update-url1.xml11
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/insecure-and-securely-updated1.xml14
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/invalid.xml1
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/mozilla-ns.xml12
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/post.xml8
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/resourceicon.xml9
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/secure-and-insecurely-updated1.xml14
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/secure-and-insecurely-updated2.xml14
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/secure-and-no-update-url1.xml11
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated-insecure-form.xml14
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated1.xml14
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated2.xml14
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated3.xml14
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/secure-localhost.xml11
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/secure-onionv2.xml11
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/secure-onionv3.xml11
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/simple.xml11
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/suggestion-alternate.xml16
-rw-r--r--toolkit/components/search/tests/xpcshell/opensearch/suggestion.xml15
-rw-r--r--toolkit/components/search/tests/xpcshell/searchconfigs/head_searchconfig.js614
-rw-r--r--toolkit/components/search/tests/xpcshell/searchconfigs/test_amazon.js361
-rw-r--r--toolkit/components/search/tests/xpcshell/searchconfigs/test_baidu.js43
-rw-r--r--toolkit/components/search/tests/xpcshell/searchconfigs/test_bing.js133
-rw-r--r--toolkit/components/search/tests/xpcshell/searchconfigs/test_distributions.js346
-rw-r--r--toolkit/components/search/tests/xpcshell/searchconfigs/test_duckduckgo.js35
-rw-r--r--toolkit/components/search/tests/xpcshell/searchconfigs/test_ebay.js290
-rw-r--r--toolkit/components/search/tests/xpcshell/searchconfigs/test_ecosia.js37
-rw-r--r--toolkit/components/search/tests/xpcshell/searchconfigs/test_google.js173
-rw-r--r--toolkit/components/search/tests/xpcshell/searchconfigs/test_mailru.js38
-rw-r--r--toolkit/components/search/tests/xpcshell/searchconfigs/test_qwant.js38
-rw-r--r--toolkit/components/search/tests/xpcshell/searchconfigs/test_rakuten.js37
-rw-r--r--toolkit/components/search/tests/xpcshell/searchconfigs/test_selector_db_out_of_date.js58
-rw-r--r--toolkit/components/search/tests/xpcshell/searchconfigs/test_yahoojp.js38
-rw-r--r--toolkit/components/search/tests/xpcshell/searchconfigs/test_yandex.js117
-rw-r--r--toolkit/components/search/tests/xpcshell/searchconfigs/xpcshell.ini34
-rw-r--r--toolkit/components/search/tests/xpcshell/simple-engines/basic/manifest.json29
-rw-r--r--toolkit/components/search/tests/xpcshell/simple-engines/engines.json27
-rw-r--r--toolkit/components/search/tests/xpcshell/simple-engines/hidden/manifest.json29
-rw-r--r--toolkit/components/search/tests/xpcshell/simple-engines/simple/manifest.json29
-rw-r--r--toolkit/components/search/tests/xpcshell/test-extensions/engines.json58
-rw-r--r--toolkit/components/search/tests/xpcshell/test-extensions/multilocale/_locales/af/messages.json20
-rw-r--r--toolkit/components/search/tests/xpcshell/test-extensions/multilocale/_locales/an/messages.json20
-rw-r--r--toolkit/components/search/tests/xpcshell/test-extensions/multilocale/favicon-af.icobin0 -> 884 bytes
-rw-r--r--toolkit/components/search/tests/xpcshell/test-extensions/multilocale/favicon-an.icobin0 -> 5430 bytes
-rw-r--r--toolkit/components/search/tests/xpcshell/test-extensions/multilocale/manifest.json23
-rw-r--r--toolkit/components/search/tests/xpcshell/test-extensions/plainengine/favicon.icobin0 -> 5430 bytes
-rw-r--r--toolkit/components/search/tests/xpcshell/test-extensions/plainengine/manifest.json58
-rw-r--r--toolkit/components/search/tests/xpcshell/test-extensions/special-engine/favicon.icobin0 -> 5430 bytes
-rw-r--r--toolkit/components/search/tests/xpcshell/test-extensions/special-engine/manifest.json40
-rw-r--r--toolkit/components/search/tests/xpcshell/test_SearchStaticData.js40
-rw-r--r--toolkit/components/search/tests/xpcshell/test_appDefaultEngine.js41
-rw-r--r--toolkit/components/search/tests/xpcshell/test_async.js40
-rw-r--r--toolkit/components/search/tests/xpcshell/test_config_attribution.js29
-rw-r--r--toolkit/components/search/tests/xpcshell/test_config_engine_params.js68
-rw-r--r--toolkit/components/search/tests/xpcshell/test_defaultEngine.js121
-rw-r--r--toolkit/components/search/tests/xpcshell/test_defaultEngine_experiments.js422
-rw-r--r--toolkit/components/search/tests/xpcshell/test_defaultEngine_fallback.js406
-rw-r--r--toolkit/components/search/tests/xpcshell/test_defaultPrivateEngine.js582
-rw-r--r--toolkit/components/search/tests/xpcshell/test_engine_alias.js31
-rw-r--r--toolkit/components/search/tests/xpcshell/test_engine_ids.js144
-rw-r--r--toolkit/components/search/tests/xpcshell/test_engine_multiple_alias.js23
-rw-r--r--toolkit/components/search/tests/xpcshell/test_engine_selector.js241
-rw-r--r--toolkit/components/search/tests/xpcshell/test_engine_selector_application.js115
-rw-r--r--toolkit/components/search/tests/xpcshell/test_engine_selector_application_distribution.js120
-rw-r--r--toolkit/components/search/tests/xpcshell/test_engine_selector_application_name.js126
-rw-r--r--toolkit/components/search/tests/xpcshell/test_engine_selector_order.js136
-rw-r--r--toolkit/components/search/tests/xpcshell/test_engine_selector_override.js181
-rw-r--r--toolkit/components/search/tests/xpcshell/test_engine_selector_remote_settings.js343
-rw-r--r--toolkit/components/search/tests/xpcshell/test_engine_set_alias.js132
-rw-r--r--toolkit/components/search/tests/xpcshell/test_getSubmission_encoding.js29
-rw-r--r--toolkit/components/search/tests/xpcshell/test_getSubmission_params.js94
-rw-r--r--toolkit/components/search/tests/xpcshell/test_identifiers.js60
-rw-r--r--toolkit/components/search/tests/xpcshell/test_ignorelist.js72
-rw-r--r--toolkit/components/search/tests/xpcshell/test_ignorelist_update.js89
-rw-r--r--toolkit/components/search/tests/xpcshell/test_initialization.js59
-rw-r--r--toolkit/components/search/tests/xpcshell/test_initialization_with_region.js130
-rw-r--r--toolkit/components/search/tests/xpcshell/test_list_json_locale.js106
-rw-r--r--toolkit/components/search/tests/xpcshell/test_list_json_no_private_default.js49
-rw-r--r--toolkit/components/search/tests/xpcshell/test_list_json_searchdefault.js70
-rw-r--r--toolkit/components/search/tests/xpcshell/test_list_json_searchorder.js66
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_timeout_xhr.js90
-rw-r--r--toolkit/components/search/tests/xpcshell/test_maybereloadengine_order.js53
-rw-r--r--toolkit/components/search/tests/xpcshell/test_migrateWebExtensionEngine.js89
-rw-r--r--toolkit/components/search/tests/xpcshell/test_missing_engine.js104
-rw-r--r--toolkit/components/search/tests/xpcshell/test_multipleIcons.js60
-rw-r--r--toolkit/components/search/tests/xpcshell/test_nodb_pluschanges.js52
-rw-r--r--toolkit/components/search/tests/xpcshell/test_notifications.js125
-rw-r--r--toolkit/components/search/tests/xpcshell/test_opensearch.js160
-rw-r--r--toolkit/components/search/tests/xpcshell/test_opensearch_icon.js63
-rw-r--r--toolkit/components/search/tests/xpcshell/test_opensearch_icons_invalid.js49
-rw-r--r--toolkit/components/search/tests/xpcshell/test_opensearch_install_errors.js67
-rw-r--r--toolkit/components/search/tests/xpcshell/test_opensearch_telemetry.js62
-rw-r--r--toolkit/components/search/tests/xpcshell/test_opensearch_update.js120
-rw-r--r--toolkit/components/search/tests/xpcshell/test_override_allowlist.js391
-rw-r--r--toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js182
-rw-r--r--toolkit/components/search/tests/xpcshell/test_policyEngine.js185
-rw-r--r--toolkit/components/search/tests/xpcshell/test_pref.js70
-rw-r--r--toolkit/components/search/tests/xpcshell/test_purpose.js83
-rw-r--r--toolkit/components/search/tests/xpcshell/test_reload_engines.js316
-rw-r--r--toolkit/components/search/tests/xpcshell/test_reload_engines_experiment.js133
-rw-r--r--toolkit/components/search/tests/xpcshell/test_reload_engines_locales.js95
-rw-r--r--toolkit/components/search/tests/xpcshell/test_remove_engine_notification_box.js392
-rw-r--r--toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js46
-rw-r--r--toolkit/components/search/tests/xpcshell/test_save_sorted_engines.js67
-rw-r--r--toolkit/components/search/tests/xpcshell/test_searchSuggest.js891
-rw-r--r--toolkit/components/search/tests/xpcshell/test_searchSuggest_cookies.js143
-rw-r--r--toolkit/components/search/tests/xpcshell/test_searchSuggest_extraParams.js50
-rw-r--r--toolkit/components/search/tests/xpcshell/test_searchSuggest_private.js54
-rw-r--r--toolkit/components/search/tests/xpcshell/test_searchTermFromResult.js303
-rw-r--r--toolkit/components/search/tests/xpcshell/test_searchUrlDomain.js21
-rw-r--r--toolkit/components/search/tests/xpcshell/test_selectedEngine.js155
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sendSubmissionURL.js90
-rw-r--r--toolkit/components/search/tests/xpcshell/test_settings.js616
-rw-r--r--toolkit/components/search/tests/xpcshell/test_settings_broken.js131
-rw-r--r--toolkit/components/search/tests/xpcshell/test_settings_duplicate.js134
-rw-r--r--toolkit/components/search/tests/xpcshell/test_settings_good.js103
-rw-r--r--toolkit/components/search/tests/xpcshell/test_settings_ignorelist.js62
-rw-r--r--toolkit/components/search/tests/xpcshell/test_settings_migration_ids.js122
-rw-r--r--toolkit/components/search/tests/xpcshell/test_settings_migration_loadPath.js130
-rw-r--r--toolkit/components/search/tests/xpcshell/test_settings_none.js50
-rw-r--r--toolkit/components/search/tests/xpcshell/test_settings_obsolete.js83
-rw-r--r--toolkit/components/search/tests/xpcshell/test_settings_persist.js89
-rw-r--r--toolkit/components/search/tests/xpcshell/test_settings_persist_diff_locale_same_name.js68
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sort_orders-no-hints.js69
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sort_orders.js91
-rw-r--r--toolkit/components/search/tests/xpcshell/test_telemetry_event_default.js293
-rw-r--r--toolkit/components/search/tests/xpcshell/test_userEngine.js56
-rw-r--r--toolkit/components/search/tests/xpcshell/test_validate_engines.js36
-rw-r--r--toolkit/components/search/tests/xpcshell/test_validate_manifests.js64
-rw-r--r--toolkit/components/search/tests/xpcshell/test_webextensions_builtin_upgrade.js252
-rw-r--r--toolkit/components/search/tests/xpcshell/test_webextensions_install.js205
-rw-r--r--toolkit/components/search/tests/xpcshell/test_webextensions_language_switch.js110
-rw-r--r--toolkit/components/search/tests/xpcshell/test_webextensions_migrate_to.js66
-rw-r--r--toolkit/components/search/tests/xpcshell/test_webextensions_normandy_upgrade.js93
-rw-r--r--toolkit/components/search/tests/xpcshell/test_webextensions_startup_remove.js83
-rw-r--r--toolkit/components/search/tests/xpcshell/test_webextensions_upgrade.js188
-rw-r--r--toolkit/components/search/tests/xpcshell/test_webextensions_valid.js165
-rw-r--r--toolkit/components/search/tests/xpcshell/xpcshell.ini196
200 files changed, 18866 insertions, 0 deletions
diff --git a/toolkit/components/search/tests/SearchTestUtils.sys.mjs b/toolkit/components/search/tests/SearchTestUtils.sys.mjs
new file mode 100644
index 0000000000..a2dc9b9e01
--- /dev/null
+++ b/toolkit/components/search/tests/SearchTestUtils.sys.mjs
@@ -0,0 +1,505 @@
+import { MockRegistrar } from "resource://testing-common/MockRegistrar.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
+ ExtensionTestUtils:
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs",
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+ SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+var gTestScope;
+
+export var SearchTestUtils = {
+ init(testScope) {
+ gTestScope = testScope;
+ this._isMochitest = !Services.env.exists("XPCSHELL_TEST_PROFILE_DIR");
+ if (this._isMochitest) {
+ this._isMochitest = true;
+ lazy.AddonTestUtils.initMochitest(testScope);
+ } else {
+ this._isMochitest = false;
+ // This handles xpcshell-tests.
+ gTestScope.ExtensionTestUtils = lazy.ExtensionTestUtils;
+ this.initXPCShellAddonManager(testScope);
+ }
+ },
+
+ /**
+ * Adds an OpenSearch based engine to the search service. It will remove
+ * the engine at the end of the test.
+ *
+ * @param {object} options
+ * The options for the new search engine.
+ * @param {string} options.url
+ * The URL of the engine to add.
+ * @param {boolean} [options.setAsDefault]
+ * Whether or not to set the engine as default automatically. If this is
+ * true, the engine will be set as default, and the previous default engine
+ * will be restored when the test exits.
+ * @param {boolean} [options.setAsDefaultPrivate]
+ * Whether or not to set the engine as default automatically for private mode.
+ * If this is true, the engine will be set as default, and the previous default
+ * engine will be restored when the test exits.
+ * @returns {Promise} Returns a promise that is resolved with the new engine
+ * or rejected if it fails.
+ */
+ async promiseNewSearchEngine({
+ url,
+ setAsDefault = false,
+ setAsDefaultPrivate = false,
+ }) {
+ // OpenSearch engines can only be added via http protocols.
+ url = url.replace("chrome://mochitests/content", "https://example.com");
+ let engine = await Services.search.addOpenSearchEngine(url, "");
+ let previousEngine = Services.search.defaultEngine;
+ let previousPrivateEngine = Services.search.defaultPrivateEngine;
+ if (setAsDefault) {
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ }
+ if (setAsDefaultPrivate) {
+ await Services.search.setDefaultPrivate(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ }
+ gTestScope.registerCleanupFunction(async () => {
+ if (setAsDefault) {
+ await Services.search.setDefault(
+ previousEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ }
+ if (setAsDefaultPrivate) {
+ await Services.search.setDefaultPrivate(
+ previousPrivateEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ }
+ try {
+ await Services.search.removeEngine(engine);
+ } catch (ex) {
+ // Don't throw if the test has already removed it.
+ }
+ });
+ return engine;
+ },
+
+ /**
+ * Returns a promise that is resolved when an observer notification from the
+ * search service fires with the specified data.
+ *
+ * @param {*} expectedData
+ * The value the observer notification sends that causes us to resolve
+ * the promise.
+ * @param {string} topic
+ * The notification topic to observe. Defaults to 'browser-search-service'.
+ * @returns {Promise}
+ * Returns a promise that is resolved with the subject of the
+ * topic once the topic with the data has been observed.
+ */
+ promiseSearchNotification(expectedData, topic = "browser-search-service") {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ if (aData != expectedData) {
+ return;
+ }
+
+ Services.obs.removeObserver(observer, topic);
+ // Let the stack unwind.
+ Services.tm.dispatchToMainThread(() => resolve(aSubject));
+ }, topic);
+ });
+ },
+
+ /**
+ * Load engines from test data located in particular folders.
+ *
+ * @param {string} [folder]
+ * The folder name to use.
+ * @param {string} [subFolder]
+ * The subfolder to use, if any.
+ * @param {Array} [config]
+ * An array which contains the configuration to set.
+ * @returns {object}
+ * An object that is a sinon stub for the configuration getter.
+ */
+ async useTestEngines(folder = "data", subFolder = null, config = null) {
+ let url = `resource://test/${folder}/`;
+ if (subFolder) {
+ url += `${subFolder}/`;
+ }
+ let resProt = Services.io
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ resProt.setSubstitution("search-extensions", Services.io.newURI(url));
+
+ const settings = await lazy.RemoteSettings(lazy.SearchUtils.SETTINGS_KEY);
+ if (config) {
+ return lazy.sinon.stub(settings, "get").returns(config);
+ }
+
+ let response = await fetch(`resource://search-extensions/engines.json`);
+ let json = await response.json();
+ return lazy.sinon.stub(settings, "get").returns(json.data);
+ },
+
+ async useMochitestEngines(testDir) {
+ // Replace the path we load search engines from with
+ // the path to our test data.
+ let resProt = Services.io
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ let originalSubstitution = resProt.getSubstitution("search-extensions");
+ resProt.setSubstitution(
+ "search-extensions",
+ Services.io.newURI("file://" + testDir.path)
+ );
+ gTestScope.registerCleanupFunction(() => {
+ resProt.setSubstitution("search-extensions", originalSubstitution);
+ });
+ },
+
+ /**
+ * Convert a list of engine configurations into engine objects.
+ *
+ * @param {Array} engineConfigurations
+ * An array of engine configurations.
+ */
+ async searchConfigToEngines(engineConfigurations) {
+ let engines = [];
+ for (let config of engineConfigurations) {
+ let engine = await Services.search.wrappedJSObject._makeEngineFromConfig(
+ config
+ );
+ engines.push(engine);
+ }
+ return engines;
+ },
+
+ /**
+ * Provides various setup for xpcshell-tests installing WebExtensions. Should
+ * be called from the global scope of the test.
+ *
+ * @param {object} scope
+ * The global scope of the test being run.
+ * @param {*} usePrivilegedSignatures
+ * How to sign created addons.
+ */
+ initXPCShellAddonManager(scope, usePrivilegedSignatures = false) {
+ let scopes =
+ lazy.AddonManager.SCOPE_PROFILE | lazy.AddonManager.SCOPE_APPLICATION;
+ Services.prefs.setIntPref("extensions.enabledScopes", scopes);
+ // Only do this once.
+ try {
+ gTestScope.ExtensionTestUtils.init(scope);
+ } catch (ex) {
+ // This can happen if init is called twice.
+ if (ex.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
+ throw ex;
+ }
+ }
+ lazy.AddonTestUtils.usePrivilegedSignatures = usePrivilegedSignatures;
+ lazy.AddonTestUtils.overrideCertDB();
+ },
+
+ /**
+ * Add a search engine as a WebExtension.
+ *
+ * Note: for tests, the extension must generally be unloaded before
+ * `registerCleanupFunction`s are triggered. See bug 1694409.
+ *
+ * This function automatically registers an unload for the extension, this
+ * may be skipped with the skipUnload argument.
+ *
+ * @param {object} [manifest]
+ * See {@link createEngineManifest}
+ * @param {object} [options]
+ * Options for how the engine is installed and uninstalled.
+ * @param {boolean} [options.setAsDefault]
+ * Whether or not to set the engine as default automatically. If this is
+ * true, the engine will be set as default, and the previous default engine
+ * will be restored when the test exits.
+ * @param {boolean} [options.setAsDefaultPrivate]
+ * Whether or not to set the engine as default automatically for private mode.
+ * If this is true, the engine will be set as default, and the previous default
+ * engine will be restored when the test exits.
+ * @param {boolean} [options.skipUnload]
+ * If true, this will skip the automatic unloading of the extension.
+ * @param {object} [files]
+ * A key value object where the keys are the filenames and their contents are
+ * the values. Used for simulating locales and other files in the WebExtension.
+ * @returns {object}
+ * The loaded extension.
+ */
+ async installSearchExtension(
+ manifest = {},
+ {
+ setAsDefault = false,
+ setAsDefaultPrivate = false,
+ skipUnload = false,
+ } = {},
+ files = {}
+ ) {
+ await Services.search.init();
+
+ let extensionInfo = {
+ useAddonManager: "permanent",
+ files,
+ manifest: this.createEngineManifest(manifest),
+ };
+
+ let extension;
+
+ let previousEngine = Services.search.defaultEngine;
+ let previousPrivateEngine = Services.search.defaultPrivateEngine;
+
+ async function cleanup() {
+ if (setAsDefault) {
+ await Services.search.setDefault(
+ previousEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ }
+ if (setAsDefaultPrivate) {
+ await Services.search.setDefaultPrivate(
+ previousPrivateEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ }
+ await extension.unload();
+ }
+
+ // Cleanup must be registered before loading the extension to avoid
+ // failures for mochitests.
+ if (!skipUnload && this._isMochitest) {
+ gTestScope.registerCleanupFunction(cleanup);
+ }
+
+ extension = gTestScope.ExtensionTestUtils.loadExtension(extensionInfo);
+ await extension.startup();
+ await lazy.AddonTestUtils.waitForSearchProviderStartup(extension);
+ let engine = Services.search.getEngineByName(manifest.name);
+
+ if (setAsDefault) {
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ }
+ if (setAsDefaultPrivate) {
+ await Services.search.setDefaultPrivate(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ }
+
+ // For xpcshell-tests we must register the unload after adding the extension.
+ // See bug 1694409 for why this is.
+ if (!skipUnload && !this._isMochitest) {
+ gTestScope.registerCleanupFunction(cleanup);
+ }
+
+ return extension;
+ },
+
+ /**
+ * Install a search engine as a system extension to simulate
+ * Normandy updates. For xpcshell-tests only.
+ *
+ * @param {object} [options]
+ * See {@link createEngineManifest}
+ */
+ async installSystemSearchExtension(options = {}) {
+ options.id = (options.id ?? "example") + "@search.mozilla.org";
+ let xpi = await lazy.AddonTestUtils.createTempWebExtensionFile({
+ manifest: this.createEngineManifest(options),
+ background() {
+ // eslint-disable-next-line no-undef
+ browser.test.sendMessage("started");
+ },
+ });
+ let wrapper = gTestScope.ExtensionTestUtils.expectExtension(options.id);
+
+ const install = await lazy.AddonManager.getInstallForURL(
+ `file://${xpi.path}`,
+ {
+ useSystemLocation: true,
+ }
+ );
+
+ install.install();
+
+ await wrapper.awaitStartup();
+ await wrapper.awaitMessage("started");
+
+ return wrapper;
+ },
+
+ /**
+ * Create a search engine extension manifest.
+ *
+ * @param {object} [options]
+ * The options for the manifest.
+ * @param {string} [options.id]
+ * The id to use for the WebExtension.
+ * @param {string} [options.name]
+ * The display name to use for the WebExtension.
+ * @param {string} [options.version]
+ * The version to use for the WebExtension.
+ * @param {string} [options.favicon_url]
+ * The favicon to use for the search engine in the WebExtension.
+ * @param {string} [options.keyword]
+ * The keyword to use for the search engine.
+ * @param {string} [options.encoding]
+ * The encoding to use for the search engine.
+ * @param {string} [options.search_url]
+ * The search URL to use for the search engine.
+ * @param {string} [options.search_url_get_params]
+ * The GET search URL parameters to use for the search engine
+ * @param {string} [options.search_url_post_params]
+ * The POST search URL parameters to use for the search engine
+ * @param {string} [options.suggest_url]
+ * The suggestion URL to use for the search engine.
+ * @param {string} [options.suggest_url_get_params]
+ * The suggestion URL parameters to use for the search engine.
+ * @param {string} [options.search_form]
+ * The search form to use for the search engine.
+ * @returns {object}
+ * The generated manifest.
+ */
+ createEngineManifest(options = {}) {
+ options.name = options.name ?? "Example";
+ options.id = options.id ?? options.name.toLowerCase().replaceAll(" ", "");
+ if (!options.id.includes("@")) {
+ options.id += "@tests.mozilla.org";
+ }
+ options.version = options.version ?? "1.0";
+ let manifest = {
+ version: options.version,
+ browser_specific_settings: {
+ gecko: {
+ id: options.id,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: options.name,
+ search_url: options.search_url ?? "https://example.com/",
+ },
+ },
+ };
+
+ if (options.default_locale) {
+ manifest.default_locale = options.default_locale;
+ }
+
+ if (options.search_url_post_params) {
+ manifest.chrome_settings_overrides.search_provider.search_url_post_params =
+ options.search_url_post_params;
+ } else {
+ manifest.chrome_settings_overrides.search_provider.search_url_get_params =
+ options.search_url_get_params ?? "?q={searchTerms}";
+ }
+
+ if (options.favicon_url) {
+ manifest.chrome_settings_overrides.search_provider.favicon_url =
+ options.favicon_url;
+ }
+ if (options.encoding) {
+ manifest.chrome_settings_overrides.search_provider.encoding =
+ options.encoding;
+ }
+ if (options.keyword) {
+ manifest.chrome_settings_overrides.search_provider.keyword =
+ options.keyword;
+ }
+ if (options.suggest_url) {
+ manifest.chrome_settings_overrides.search_provider.suggest_url =
+ options.suggest_url;
+ }
+ if (options.suggest_url) {
+ manifest.chrome_settings_overrides.search_provider.suggest_url_get_params =
+ options.suggest_url_get_params;
+ }
+ if (options.search_form) {
+ manifest.chrome_settings_overrides.search_provider.search_form =
+ options.search_form;
+ }
+ if (options.favicon_url) {
+ manifest.chrome_settings_overrides.search_provider.favicon_url =
+ options.favicon_url;
+ }
+ return manifest;
+ },
+
+ /**
+ * A mock idleService that allows us to simulate RemoteSettings
+ * configuration updates.
+ */
+ idleService: {
+ _observers: new Set(),
+
+ _reset() {
+ this._observers.clear();
+ },
+
+ _fireObservers(state) {
+ for (let observer of this._observers.values()) {
+ observer.observe(observer, state, null);
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIUserIdleService"]),
+ idleTime: 19999,
+
+ addIdleObserver(observer, time) {
+ this._observers.add(observer);
+ },
+
+ removeIdleObserver(observer, time) {
+ this._observers.delete(observer);
+ },
+ },
+
+ /**
+ * Register the mock idleSerice.
+ */
+ useMockIdleService() {
+ let fakeIdleService = MockRegistrar.register(
+ "@mozilla.org/widget/useridleservice;1",
+ SearchTestUtils.idleService
+ );
+ gTestScope.registerCleanupFunction(() => {
+ MockRegistrar.unregister(fakeIdleService);
+ });
+ },
+
+ /**
+ * Simulates an update to the RemoteSettings configuration.
+ *
+ * @param {object} [config]
+ * The new configuration.
+ */
+ async updateRemoteSettingsConfig(config) {
+ if (!config) {
+ let settings = lazy.RemoteSettings(lazy.SearchUtils.SETTINGS_KEY);
+ config = await settings.get();
+ }
+ const reloadObserved =
+ SearchTestUtils.promiseSearchNotification("engines-reloaded");
+ await lazy.RemoteSettings(lazy.SearchUtils.SETTINGS_KEY).emit("sync", {
+ data: { current: config },
+ });
+
+ this.idleService._fireObservers("idle");
+ await reloadObserved;
+ },
+};
diff --git a/toolkit/components/search/tests/xpcshell/data/bigIcon.ico b/toolkit/components/search/tests/xpcshell/data/bigIcon.ico
new file mode 100644
index 0000000000..f22522411d
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/bigIcon.ico
Binary files differ
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-app/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-app/manifest.json
new file mode 100644
index 0000000000..14bc27bdd4
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-app/manifest.json
@@ -0,0 +1,24 @@
+{
+ "name": "TestEngineApp",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine-app@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "description": "A test search engine installed in the application directory",
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "TestEngineApp",
+ "search_url": "https://localhost/",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-chromeicon/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-chromeicon/manifest.json
new file mode 100644
index 0000000000..d2896b78f8
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-chromeicon/manifest.json
@@ -0,0 +1,23 @@
+{
+ "name": "engine-chromeicon",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine-chromeicon@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "engine-chromeicon",
+ "search_url": "https://www.google.com/search",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-diff-name/_locales/en/messages.json b/toolkit/components/search/tests/xpcshell/data/engine-diff-name/_locales/en/messages.json
new file mode 100644
index 0000000000..07580b8ea5
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-diff-name/_locales/en/messages.json
@@ -0,0 +1,8 @@
+{
+ "extensionName": {
+ "message": "engine-diff-name-en"
+ },
+ "searchUrl": {
+ "message": "https://en.wikipedia.com/search"
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-diff-name/_locales/gd/messages.json b/toolkit/components/search/tests/xpcshell/data/engine-diff-name/_locales/gd/messages.json
new file mode 100644
index 0000000000..de01d16bf0
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-diff-name/_locales/gd/messages.json
@@ -0,0 +1,8 @@
+{
+ "extensionName": {
+ "message": "engine-diff-name-gd"
+ },
+ "searchUrl": {
+ "message": "https://gd.wikipedia.com/search"
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-diff-name/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-diff-name/manifest.json
new file mode 100644
index 0000000000..3c80765f61
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-diff-name/manifest.json
@@ -0,0 +1,21 @@
+{
+ "name": "engine-diff-name",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine-diff-name@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "icons": {
+ "16": "favicon.png"
+ },
+ "default_locale": "en",
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "__MSG_extensionName__",
+ "search_url": "__MSG_searchUrl__"
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-fr.xml b/toolkit/components/search/tests/xpcshell/data/engine-fr.xml
new file mode 100644
index 0000000000..4bb4426a12
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-fr.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Test search engine (fr)</ShortName>
+<Description>A test search engine (based on Google search for a different locale)</Description>
+<InputEncoding>ISO-8859-1</InputEncoding>
+<Url type="text/html" method="GET" template="http://www.google.fr/search">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="ie" value="iso-8859-1"/>
+ <Param name="oe" value="iso-8859-1"/>
+</Url>
+<SearchForm>http://www.google.fr/</SearchForm>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-fr/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-fr/manifest.json
new file mode 100644
index 0000000000..cc895d26d9
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-fr/manifest.json
@@ -0,0 +1,32 @@
+{
+ "name": "Test search engine (fr)",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine-fr@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "description": "A test search engine (based on Google search for a different locale)",
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "Test search engine (fr)",
+ "search_url": "https://www.google.fr/search",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "ie",
+ "value": "iso-8859-1"
+ },
+ {
+ "name": "oe",
+ "value": "iso-8859-1"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-override/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-override/manifest.json
new file mode 100644
index 0000000000..a894be0a41
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-override/manifest.json
@@ -0,0 +1,24 @@
+{
+ "name": "bug645970",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine-override@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "description": "override",
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "bug645970",
+ "search_url": "https://searchtest.local",
+ "params": [
+ {
+ "name": "search",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-pref/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-pref/manifest.json
new file mode 100644
index 0000000000..a876520bc2
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-pref/manifest.json
@@ -0,0 +1,28 @@
+{
+ "name": "engine-pref",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine-pref@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "engine-pref",
+ "search_url": "https://www.google.com/search",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "code",
+ "condition": "pref",
+ "pref": "code"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-purposes/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-purposes/manifest.json
new file mode 100644
index 0000000000..fc709063e1
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-purposes/manifest.json
@@ -0,0 +1,62 @@
+{
+ "name": "Test Engine With Purposes",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine-purposes@search.mozilla.org"
+ }
+ },
+ "description": "A test search engine with purposes",
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "Test Engine With Purposes",
+ "search_url": "https://www.example.com/search",
+ "params": [
+ {
+ "name": "form",
+ "condition": "purpose",
+ "purpose": "keyword",
+ "value": "MOZKEYWORD"
+ },
+ {
+ "name": "form",
+ "condition": "purpose",
+ "purpose": "contextmenu",
+ "value": "MOZCONTEXT"
+ },
+ {
+ "name": "form",
+ "condition": "purpose",
+ "purpose": "newtab",
+ "value": "MOZNEWTAB"
+ },
+ {
+ "name": "form",
+ "condition": "purpose",
+ "purpose": "searchbar",
+ "value": "MOZSEARCHBAR"
+ },
+ {
+ "name": "form",
+ "condition": "purpose",
+ "purpose": "homepage",
+ "value": "MOZHOMEPAGE"
+ },
+ {
+ "name": "pc",
+ "value": "FIREFOX"
+ },
+ {
+ "name": "channel",
+ "condition": "pref",
+ "pref": "testChannelEnabled"
+ },
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-purpose/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-purpose/manifest.json
new file mode 100644
index 0000000000..ed4a609e7c
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-purpose/manifest.json
@@ -0,0 +1,41 @@
+{
+ "name": "engine-rel-searchform-purpose",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine-rel-searchform-purpose@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "engine-rel-searchform-purpose",
+ "search_url": "https://www.google.com/search",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "channel",
+ "condition": "purpose",
+ "purpose": "contextmenu",
+ "value": "rcs"
+ },
+ {
+ "name": "channel",
+ "condition": "purpose",
+ "purpose": "keyword",
+ "value": "fflb"
+ },
+ {
+ "name": "channel",
+ "condition": "purpose",
+ "purpose": "searchbar",
+ "value": "sb"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-reordered/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-reordered/manifest.json
new file mode 100644
index 0000000000..cc3fc95430
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-reordered/manifest.json
@@ -0,0 +1,40 @@
+{
+ "name": "Test search engine (Reordered)",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine-reordered@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "description": "A test search engine (based on Google search)",
+ "icons": {
+ "16": "favicon.ico"
+ },
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "Test search engine (Reordered)",
+ "search_url": "https://www.google.com/search",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "channel",
+ "condition": "purpose",
+ "purpose": "contextmenu",
+ "value": "rcs"
+ },
+ {
+ "name": "channel",
+ "condition": "purpose",
+ "purpose": "keyword",
+ "value": "fflb"
+ }
+ ],
+ "suggest_url": "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}"
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/_locales/en/messages.json b/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/_locales/en/messages.json
new file mode 100644
index 0000000000..1cc3f68ee1
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/_locales/en/messages.json
@@ -0,0 +1,8 @@
+{
+ "extensionName": {
+ "message": "engine-resourceicon"
+ },
+ "searchUrl": {
+ "message": "https://www.google.com/search"
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/_locales/gd/messages.json b/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/_locales/gd/messages.json
new file mode 100644
index 0000000000..3c02e6a2af
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/_locales/gd/messages.json
@@ -0,0 +1,8 @@
+{
+ "extensionName": {
+ "message": "engine-resourceicon-gd"
+ },
+ "searchUrl": {
+ "message": "https://www.google.com/search"
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/manifest.json
new file mode 100644
index 0000000000..dc62336145
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/manifest.json
@@ -0,0 +1,21 @@
+{
+ "name": "engine-resourceicon",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine-resourceicon@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "icons": {
+ "16": "favicon.png"
+ },
+ "default_locale": "en",
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "__MSG_extensionName__",
+ "search_url": "__MSG_searchUrl__"
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-same-name/_locales/en/messages.json b/toolkit/components/search/tests/xpcshell/data/engine-same-name/_locales/en/messages.json
new file mode 100644
index 0000000000..ee808e7a62
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-same-name/_locales/en/messages.json
@@ -0,0 +1,8 @@
+{
+ "extensionName": {
+ "message": "engine-same-name"
+ },
+ "searchUrl": {
+ "message": "https://www.google.com/search?q={searchTerms}"
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-same-name/_locales/gd/messages.json b/toolkit/components/search/tests/xpcshell/data/engine-same-name/_locales/gd/messages.json
new file mode 100644
index 0000000000..476a9e56cc
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-same-name/_locales/gd/messages.json
@@ -0,0 +1,8 @@
+{
+ "extensionName": {
+ "message": "engine-same-name"
+ },
+ "searchUrl": {
+ "message": "https://www.example.com/search?q={searchTerms}"
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-same-name/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-same-name/manifest.json
new file mode 100644
index 0000000000..dc8a4c5d45
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-same-name/manifest.json
@@ -0,0 +1,21 @@
+{
+ "name": "engine-same-name",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine-same-name@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "icons": {
+ "16": "favicon.png"
+ },
+ "default_locale": "en",
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "__MSG_extensionName__",
+ "search_url": "__MSG_searchUrl__"
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-system-purpose/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-system-purpose/manifest.json
new file mode 100644
index 0000000000..d268af8eab
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-system-purpose/manifest.json
@@ -0,0 +1,35 @@
+{
+ "name": "engine-system-purpose",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine-system-purpose@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "engine-system-purpose",
+ "search_url": "https://www.google.com/search",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "channel",
+ "condition": "purpose",
+ "purpose": "searchbar",
+ "value": "sb"
+ },
+ {
+ "name": "channel",
+ "condition": "purpose",
+ "purpose": "system",
+ "value": "sys"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine.xml b/toolkit/components/search/tests/xpcshell/data/engine.xml
new file mode 100644
index 0000000000..a665e46b0b
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Test search engine</ShortName>
+<Description>A test search engine (based on Google search)</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://suggestqueries.google.com/complete/search?output=firefox&amp;client=firefox&amp;hl={moz:locale}&amp;q={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.google.com/search">
+ <Param name="q" value="{searchTerms}"/>
+ <!-- Dynamic parameters -->
+ <MozParam name="channel" condition="purpose" purpose="contextmenu" value="rcs"/>
+ <MozParam name="channel" condition="purpose" purpose="keyword" value="fflb"/>
+</Url>
+<SearchForm>http://www.google.com/</SearchForm>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine/manifest.json
new file mode 100644
index 0000000000..30d221f388
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine/manifest.json
@@ -0,0 +1,40 @@
+{
+ "name": "Test search engine",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "description": "A test search engine (based on Google search)",
+ "icons": {
+ "16": "favicon.ico"
+ },
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "Test search engine",
+ "search_url": "https://www.google.com/search",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "channel",
+ "condition": "purpose",
+ "purpose": "contextmenu",
+ "value": "rcs"
+ },
+ {
+ "name": "channel",
+ "condition": "purpose",
+ "purpose": "keyword",
+ "value": "fflb"
+ }
+ ],
+ "suggest_url": "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}"
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engine2.xml b/toolkit/components/search/tests/xpcshell/data/engine2.xml
new file mode 100644
index 0000000000..9957bfdf48
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine2.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
+ <ShortName>A second test engine</ShortName>
+ <Description>A second test search engine (based on DuckDuckGo)</Description>
+ <InputEncoding>UTF-8</InputEncoding>
+ <LongName>A second test search engine (based on DuckDuckGo)</LongName>
+ <Image width="16" height="16">data:image/vnd.microsoft.icon;base64,AAABAAMAEBAAAAEACABoBQAANgAAACAgAAABAAgAqAgAAJ4FAAAwMAAAAQAIAKgOAABGDgAAKAAAABAAAAAgAAAAAQAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUvgAAFMAAABfDAAAYwQAAGscAAB7KAAAgzAAAIcwAACHNAAAizgAAI9AAACTQAAAnzAAAJtMAACnXABktyAAALtgAAC/bAAAx3QAAM+AAADbjAAA34wAAOOQAADzYAAA65wAWPdcAADvoAABG1QC6cg0AAFXdAERS0AAAVd8AAF3cAABp5wAAcOUATGziAAB36AAAeugATHLqAF165ABlg+0A0qRkAACS7wAAlO8AeI7nAIGX6gCVndwAAKX1AACr9gAAr/YAALX4AAC3+QCdrvIAALz6AAC9+QAAv/kAAMD7AADH+wAAyvwAAND9ALHB9QAA0vwA5c68AADT/gAA1P0AANT+AADU/wAA1/8AANj/AADZ/wAA3P8AAN3/AGrV+wAA3/8AAOH/AMrS9ADs3tYA39zmAOHj8AB+6v8A5ur6AKDw/wCq8f8A9fDtAPby7wD38vAA6/D8APfz8QD69O4A8PP8APL0/AD69/IA+/fzAPr39QDd+f8A+/n1APb4/QD7+fgA+/r5AOz8/gD1/P8A+vz/AP7+/QD//v8A////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABpLGhoaGhQHgEAAQEBAQFpGUtoaGhoSw8CAwICAgICAiNQaGhoaC0EBAwgIBsEBAQnWmhoaGgnIjY5PUBHOyUFLWhoaGheR0MdBgYGByQrCEtoaGhoT0I3CQkJCQkJCQlQaGhoaFI/QTIzNSoXCgsLWmhoaGhoUUVEQ0RKRDEfDWhoXFxoaGhjZWRILzpJRjhoXBwcWGhoaFtbYg4QIS8waFwcKVhoaF8cHF8REREREWhnXFxoaGhfHClOEhISEhJVU2hoaGhoaF9fNBMTExMTVz5MZmhoaFdMTSYUFBQUFDxhVGhoaGhdPi4VFhYWFhZpVmBoaGhoWSgYGhoaGhppgAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAEAACgAAAAgAAAAQAAAAAEACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFL8AABTAAAEWvAABF74AABbCAAAXwwABGMAAABjDAAAYxAAAGsUAABrGAAAbxgAAG8cAAh3BAAEcxQAAHMgAAR3HAAgewgAAHcoAAB/LAAAgzAAFIckAiEYiAAAhzgAAI88AACPQAAAk0AAAJc8AACXSAAUmzQAAJ9IAACfTABAqxwAAKNQAjE0rAAAp1QAAKdYAACrXAAAr2AAGLdMAAC3ZAAAu2wAAL9wAADPVAAAx3QAAMt8AADPgAAA04QAAONQAADnXAAA24wAGN94ACjfdAAA34wAAN+QACznbAAA45QAJOeAAFTzUABY81AAAOeYAFj3UAAo74AAAP9YAADrnAAs74AAAPtoABT/pAABF2wAARd0AAEbaAABI1gAASdoAEETmAABJ2wAAS98AAFHfAAFV3ABTc2EAWYA+AAFa4gAAXd0AAF3hADyeAAABZd4AAWTjADZf5wABZuIAQKETAD2jDwBGZ9gARKEYADhk6wA8pBMAPKUVAK+DbABKbN8ATGziAAF15ABTpioAaKE1AEGtJAABfOkAOrIiAH6hRQABgOkAAYLmAAGC5wACgucAXXrkAAGE5wBEr0AAAYToAAGG6gA9s0AAObkuAD20QQA5ui8AaIHhAEe3OABxgeAAAo3sAFasbABshukAOcA5AGmI7gABleoAbovsAAKZ7QCFtmEAAp7vAD7CagCBl+oAPsNuAAKo8QACqvIAA6zyAAOs9AACr/QAkKfxAGLJlQBfypUAZMqWAAO49QDSuawAc8qaAMPCngADwfgAA8H5AAPE+QAEyPsABMr7AATO/AAE0PwABND9AATR/QAE0v0ACNP9ABvW/QDj1MwAMtr9ADvc/QDK0vQA59rTAOjb1ADp3tcA6+DaAG/l/gB15v4A29/0AO3k3wDu5eAA4eT3AJDr/gDy6+cA5uj3APLs6ADj6PsA8+zpAObq+gDz7uoA8+7rANHt+gD07usA6e35APbx7wDy9PwA5fr/APv7+wD2+/8A7fz/APz8+gDw/P8A/fz7APv8/gD9/f4A+P7/AP/+/gD+/v8A////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyMgAAAAAPYyDb4HHx8fHw05dUwAAAAAAAAAAAAAAyMjIAQEBAQFhkYNyXmV3Y2h6dF1TAgEBAQEBAQEBAQEByAQEBAQEBG2Og3JedXxnWYV0XVMDBAQEBAQEBAQEBAQEBQUFBQURhIyDcl5zfGdbhXRdUwYFBQUFBQUFBQUFBQUICAgICDuijYNyXmV3ZE96dF1TBwgICAgICAgICAgICAoKCgoKYbOMg5K/x8esCg5OWFMJCgoKCgoKCgoKCgoKDAwMDAxtusfHx8fHx3gMDAwLDQwMDAwMDAwMDAwMDAwPDw8PEITHx8fHx8fHFQ8PDw8PDw8PDw8PDw8PDw8PDxISEhIgosfHx8fHx8YSEhISEhISEhISEhISEhISEhISExMTEzuzx8fHx8fHrxMTExMTExMTExMTExMTExMTExMUFBQUYbrHx8fHx8epFBQUboiXnJqGVEcUFBQUFBQUFBcXFxdtx8fHx8fHx7YbbJyYj2tigpObnIAXFxcXFxcXGBgYHYTHx8fHx8e7npybcE0wGBgYP1F+ahgYGBgYGBgaGho6osfHx8fHx6icnIcZGhoaGhoaGhoaGhoaGhoaGhwcHGGzx8fHx8fHp5ycnEgeRjErHBwcHBwcHBwcHBwcISEhbbrHx8fHx8e+oZycnJycnJyViVdEISEhISEhISEhIR+Ex8fHx8fHx8fEraCdnJycnJycnJZxSiEhISEhISMjJ6LHx8fHx8fHx8fHx8fAvcBCVXmUnJyZUiMjIyMjJSU6s8fHx6Wlx8fHx8fHx8fHxyUkJUxpmZuKJSUlJSUmJmG6x8WjIhaqx8fHx8fHtLTHJiYmJkVQZksmJiYmJigobcXHx6QiX6rHx8fHx7AiFrUoKCgoKCgoKCgoKCgoKSmEx8fHx6urx8fHx8fHsiJfeykpKSkpKSkpKSkpKSkqKoTHt8fHx8fHx8fHx8fHubk0KioqKioqKioqKioqKiwsbcemx8fHx8fHx8fHwcfHuCwsLCwsLCwsLCwsLCwsLi43s8eQkJ/Hx8fHx8eukJA5Li4uLi4uLi4uLi4uLi4uLi2Ex8fHx8fHx8fHx8fHfy4uLi4uLi4uLi4uLi4uLi8vLzOzx8fHx8fHx8fHx4svLy8vLy8vLy8vLy8vLy8vMjIyMkGEvMfHx8fHx7FWMjIyMjIyMjIyMjIyMjIyMjI2NjY2MjI+drPHx8d9NTY2NjY2NjY2NjY2NjY2NjY2Njg4NlrHx8fHx8fGSTg4ODg4ODg4ODg4ODg4ODg4ODg4yEBAPGDHx8fCXEBAQEBAQEBAQEBAQEBAQEBAQEBAQMjIyENDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0PIyMAAAAOAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAHAAAADKAAAADAAAABgAAAAAQAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARwQAAE8IAABXEAAAXxQAAGcYAARvHAAAfwAAAHsoAFSucAIdGHgAAIs0Ai0kjAAAlzwAHJc8AID59AAAo0gCPTSgAACnTACQ8lwAYMsgAADDaAAI01AA+WkgAADTeABU4zgAEM+YAl1o3AAg52QAANukAADfqAAA65AA5Y0cAAEDYABc+3QAARtYANEPPAJ1lQgA6eSUAGUbeAAFK2wAVRucAMknWAAJQ2gA8hBkANoQfAB9M5QAXTeYANo0FAABT3gAmUeEAVoMvAEaYAAAAXeIASJIcAABg3gA9WeEAPZ4AADefBACneGMAA2TiAAFn3gAwX+oAU4ZkADqiDwAAauIArH9hAKx9aABWZtYAW4pfAFJp2gAAcOIAX2raAEOkJABJoi4AXKErAEtt4ABGpykANa0bAElu6gA6qiYAaXLbAAZ66ABFricAPq4tAFZ24wBseNoAeaVCALiPdwBtedsANrgeAF955gBBsjIAY6dMAFx86gBYfu0AAIjrAD61RwBCuzMAZILpAAKM6QCipWMAYrZCAL+bhwA3wjQAaIbuADy6UABrifEAAJXtAHCL6wBZtmgATLduAG2O8AACnO8APMFkAIuW4wCltXMAq7J+AIKX6AB9mOsAyamaAImb5AA5xXMAi6DjAACq9ACsuYgAT8iDAAaw8wBBzIIAjqbyAACz9wCVqu0AmK3xAJ+u8AClru4AC8P2AKa48wAAx/wAAMv5ALvQtgAJz/4AAND/ALPB9QDDzscAANP8AMDF7QAQ1PwAwsz3ACna/AC9zfoANtv8AObYzACr4sYA6NrOAMvS8wCu5M8ATuD/AGDi/wCy59MAaOP/ANPn1ABp5vwA5eHjAOzk2wDU3vgAwurYAIHq/ADr6t0Akev/AObm8wDh5vYA9e3kAOTq+gDz8OsAsPH/AOjt/QC38v8A6u//AN327QDx8f4A+PT2AM73/gDz9foA+/jzAPX2/AD8+fQA3fr8AP369QD4+v8A+v37AP/++AD8//4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC/vwAAAAAAAAAAAAAAAAF2obO9vr6+vr6+vr28RwAAAAAAAAAAAAAAAAAAAAAAv7+/AAYAAAAAAAAAAAABAgGZfW18vr6+vr6+vr6pAAAAAAAAAAAAAAAAAAAAAQAGAL8BAQEBQ5Cyvru+vb1QAxiff3FgSWS+vr6+vr5HAggWDgETvb67vr2+vrWQQwEGAQEBAQFyvlUTAAEBAQEAAUukfXFgTzhMYFJKc44+ST9IKwEBAQEBAQEBAQFVvnIBAQEBAUO+IwEBAQEAAQEBAVSdf3FgT0hnZ1lNSHlpWz85LwEBAQEBAQEBAQEBI7tDAQECApBYAgICAgICAgICAnWaf3FgUjhnZ2FPP31pUzhILwICAgICAgICAgICAliQAgICArUCAgICAgICAgICApmaf3FgTzlnZ1lNSHlpUzg5LwICAgICAgICAgICAgK1AgICAr0CAgICAgICAgICS6mXf3FgTzNWZVtPMm5pWz9ILAICAgICAgICAgICAgK7AgICAr4CAgICAgICAgICVLWXf3FcdL6+vr6FBAJEUzg4JQICAgICAgICAgICAgK9AgICArsCAgICAgICAgICdb6xiqa+vr6+vrMpCgICEjU4HwICAgICAgICAgICAgK7AgIDA74DAwMDAwMDAwMDmb6+vr6+vr6+vrAKAwMDAwMSAwMDAwMDAwMDAwMDAwO+AwMDA7sDAwMDAwMDAwMDqb6+vr6+vr6+voUDAwMDAwMDAwMDAwMDAwMDAwMDAwO9AwMFBb4FBQUFBQUFBQVLtb6+vr6+vr6+vkUFBQUFBQUFBQUFBQUFBQUFBQUFBQW+BQUFBbsFBQUFBQUFBQVavr6+vr6+vr6+vikFBQUFBQUFBQUFBQUFBQUFBQUFBQW9BQUHB7sHBwcHBwcHBwd1vr6+vr6+vr6+vgcHBwcHBwcHBwcHBwcHBwcHBwcHBwe+BwcHB70HBwcHBwcHBweZvr6+vr6+vr6+vgcHBwciUXuIj4+PhmsqBwcHBwcHBwe+BwcKCr4MCgoKCgoKCgqrvr6+vr6+vr6+vgoKY4uRj5GRkYyPi4uRiDYKCgoKCgq7CgoKCr4MCgoKCgoKCku1vr6+vr6+vr6+r4aPi4twOycgIjBAcIaLj488CgoKCgq9CgoKCr4KCgoKCgoKClS+vr6+vr6+vr60kYyMiyoKCgoKCgoKCgoVKkYVCgoKCgq9CgoMDLsMDAwMDAwMDHW+vr6+vr6+vrubjI+RYw0KCgoKCgoMCgwMDAwMDAwMCgy+CgwMDL4MDAwMDAwMDJm+vr6+vr6+vr2Tj4+PiBUMDAwMDAwMDAwMDAwMDAwMDAy9DwwMDL4MDAwMDAwMIam+vr6+vr6+vrugi4+Mj4uIi4yIflEgDAwMDAwMDAwMDAy8DAwMDL4MDAwMDAwMS7W+vr6+vr6+vr6+npGLj4yPj4+Pj4+PiF8gDAwMDAwMDAy+DAwPDL0PDwwPDA8PWr6+vr6+vr6+vr6+vq2Tj4+Lj4yPj5GLj4+PgTsPDA8MDwy9DA8PD74PDw8PDw8Pdb6+vr6+vr6+vr6+vr67ua2npZyVjI+LkY+Rj4+INA8PDw++Dw8PD70PDw8PDw8Pmb6+vr6+vr6+vr6+vr69vr6+vr69VCpffoyLjI+Jj4EPDw++Dw8PD70PDw8PDw8Pq76+vqEaGqq+vr6+vr6+vr68vr6+NxEPDw82e5GMiVEPDw++Dw8REbwRERERERFLtb6+vgsQCzq+vr6+vr6+vpYaGqy7ERERERERERERERERERG9EREUEb4RFBEUERRdvr6+vhAQoma+vr6+vr6+vgkaCTq+FBEUERQRFBEUERQRFBG+ERQUFL4UFBQUFBR1vr6+vqpBQra+vr6+vr6+uxAQoWa+FBQUFBQUFBQUFBQUFBS9FBQUFL4UFBQUFBR2vr6+vr6+vr6+vr6+vr6+vqpCQrayFBQUFBQUFBQUFBQUFBS+FBQUFL4UFBQUFBRivr6qvr6+vr6+vr6+vr6+vr6+vr6CFBQUFBQUFBQUFBQUFBS9FBQXFL4UFxQXFBdLvrNXvr6+vr6+vr6+vr6+vr6+vr4UFxQXFBcUFxQXFBcUFxS+FBcUF74XFBcUFxQXvr62JFd3s72+vr6+vr67rLO7mKgXFBcUFxQXFBcUFxQXFBe+FxQXF74XFxcXFxcXrr6+vrq9vr6+vr6+vr6+uCRXljEXFxcXFxcXFxcXFxcXFxe+FxcXF74XFxcXFxcXhL2+vr6+vr6+vr6+vr6+vb69gxcXFxcXFxcXFxcXFxcXFxe+FxcXF74XFxcXFxcXG667vr6+vr6+vr6+vr6+vrupFxcXFxcXFxcXFxcXFxcXFxe+FxcXHr4eFx4XHhceFx68vb6+vr6+vr6+vr69vrUXHhceFx4XHhceFx4XHhceFx6+HhceF74XHhceFx4XHhceeLu9vb6+vr6+vb6zhxceFx4XHhceFx4XHhceFx4XHhe+Fx4eHr4eHh4eHh4eHh4eHhctXZm+vr6+sGIeHh4eHh4eHh4eHh4eHh4eHh4eHh69Hh4eHr0eHh4eHh4eHmJsdoKNo7u+vr61LR4eHh4eHh4eHh4eHh4eHh4eHh4eHh69Hh4eHLUcHhweHB4cG6u8vr6+vr6+voMeHB4cHhweHB4cHhweHB4cHhweHB4cHhy1HB4cHpJqHB4cHhweHCaZvr6+vr2DLhwcHhweHB4cHhweHB4cHhweHB4cHhweHGqSHhwcHF6+PRwcHBwcHlR6hINoTigZGRwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcPb5eHBwcHByAvW8cHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBxvvYAcHBwcHBwcXpS3vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vreUXhwcHBy/HBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHL+/vx0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dv7/AAAAAAAMAAIAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAQAAwAAAAAADAAA=</Image>
+ <Url type="text/html" method="get" template="https://duckduckgo.com/?q={searchTerms}"/>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine2/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine2/manifest.json
new file mode 100644
index 0000000000..7dd4b15931
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine2/manifest.json
@@ -0,0 +1,18 @@
+{
+ "name": "A second test engine",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine2@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "description": "A second test search engine (based on DuckDuckGo)",
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "A second test engine",
+ "search_url": "https://duckduckgo.com/?q={searchTerms}"
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engineImages.xml b/toolkit/components/search/tests/xpcshell/data/engineImages.xml
new file mode 100644
index 0000000000..65b550b31b
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engineImages.xml
@@ -0,0 +1,22 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>IconsTest</ShortName>
+ <Description>IconsTest. Search by Test.</Description>
+ <InputEncoding>UTF-8</InputEncoding>
+ <Image width="16" height="16">data:image/x-icon;base64,ico16</Image>
+ <Image width="32" height="32">data:image/x-icon;base64,ico32</Image>
+ <Image width="74" height="74">data:image/png;base64,ico74</Image>
+ <Url type="application/x-suggestions+json" template="http://api.bing.com/osjson.aspx">
+ <Param name="query" value="{searchTerms}"/>
+ <Param name="form" value="MOZW"/>
+ </Url>
+ <Url type="text/html" method="GET" template="http://www.bing.com/search">
+ <Param name="q" value="{searchTerms}"/>
+ <MozParam name="pc" condition="pref" pref="ms-pc"/>
+ <Param name="form" value="MOZW"/>
+ </Url>
+ <SearchForm>http://www.bing.com/search</SearchForm>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engineImages/manifest.json b/toolkit/components/search/tests/xpcshell/data/engineImages/manifest.json
new file mode 100644
index 0000000000..b5366a6eb6
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engineImages/manifest.json
@@ -0,0 +1,37 @@
+{
+ "name": "IconsTest",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engineImages@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "description": "IconsTest. Search by Test.",
+ "icons": {
+ "16": "data:image/x-icon;base64,ico16"
+ },
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "IconsTest",
+ "search_url": "https://www.bing.com/search",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "form",
+ "value": "MOZW"
+ },
+ {
+ "name": "pc",
+ "condition": "pref",
+ "pref": "ms-pc"
+ }
+ ],
+ "suggest_url": "https://api.bing.com/osjson.aspxquery={searchTerms}&form=MOZW"
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engineMaker.sjs b/toolkit/components/search/tests/xpcshell/data/engineMaker.sjs
new file mode 100644
index 0000000000..4b32003dcf
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engineMaker.sjs
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Dynamically create an OpenSearch search engine offering search suggestions
+ * via searchSuggestions.sjs.
+ *
+ * The engine is constructed by passing a JSON object with engine details as the query string.
+ */
+
+function handleRequest(request, response) {
+ let engineData = JSON.parse(unescape(request.queryString).replace("+", " "));
+
+ if (!engineData.baseURL) {
+ response.setStatusLine(request.httpVersion, 500, "baseURL required");
+ return;
+ }
+
+ engineData.name = engineData.name || "Generated test engine";
+ engineData.description =
+ engineData.description || "Generated test engine description";
+ engineData.method = engineData.method || "GET";
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ createOpenSearchEngine(response, engineData);
+}
+
+/**
+ * Create an OpenSearch engine for the given base URL.
+ *
+ * @param {Response} response
+ * The response object to write the engine to.
+ * @param {object} engineData
+ * Information about the search engine to write to the response.
+ */
+function createOpenSearchEngine(response, engineData) {
+ let params = "";
+ let queryString = "";
+ if (engineData.method == "POST") {
+ params = "<Param name='q' value='{searchTerms}'/>";
+ } else {
+ queryString = "?q={searchTerms}";
+ }
+ let type = "type='application/x-suggestions+json'";
+ if (engineData.alternativeJSONType) {
+ type = "type='application/json' rel='suggestions'";
+ }
+ let image = "";
+ if (engineData.image) {
+ image = `<Image width="16" height="16">${engineData.baseURL}${engineData.image}</Image>`;
+ }
+ let updateFile = "";
+ if (engineData.updateFile) {
+ updateFile = `<Url type="application/opensearchdescription+xml"
+ rel="self"
+ template="${engineData.baseURL}${engineData.updateFile}" />
+ `;
+ }
+
+ let result = `<?xml version='1.0' encoding='utf-8'?>
+<OpenSearchDescription xmlns='http://a9.com/-/spec/opensearch/1.1/'>
+ <ShortName>${engineData.name}</ShortName>
+ <Description>${engineData.description}</Description>
+ <InputEncoding>UTF-8</InputEncoding>
+ <LongName>${engineData.name}</LongName>
+ ${image}
+ <Url ${type} method='${engineData.method}'
+ template='${engineData.baseURL}searchSuggestions.sjs${queryString}'>
+ ${params}
+ </Url>
+ <Url type='text/html' method='${engineData.method}'
+ template='${engineData.baseURL}${queryString}'/>
+ ${updateFile}
+</OpenSearchDescription>
+`;
+ response.write(result);
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engines-no-order-hint.json b/toolkit/components/search/tests/xpcshell/data/engines-no-order-hint.json
new file mode 100644
index 0000000000..85c89fb388
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engines-no-order-hint.json
@@ -0,0 +1,83 @@
+{
+ "data": [
+ {
+ "webExtension": {
+ "id": "engine@search.mozilla.org"
+ },
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "default": "yes"
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "engine-rel-searchform-purpose@search.mozilla.org"
+ },
+ "orderHint": 1000,
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "excluded": { "locales": { "matches": ["de", "fr"] } },
+ "default": "no"
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "engine-chromeicon@search.mozilla.org"
+ },
+ "orderHint": 1000,
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "excluded": { "locales": { "matches": ["de", "fr"] } },
+ "default": "no"
+ },
+ {
+ "included": { "regions": ["ru"] },
+ "default": "no"
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "engine-resourceicon@search.mozilla.org"
+ },
+ "appliesTo": [
+ {
+ "included": { "locales": { "matches": ["en-US", "fr"] } },
+ "excluded": {
+ "regions": ["ru"]
+ },
+ "default": "no"
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "engine-reordered@search.mozilla.org"
+ },
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "excluded": { "locales": { "matches": ["de", "fr"] } },
+ "default": "no"
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "engine-pref@search.mozilla.org"
+ },
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "excluded": { "locales": { "matches": ["de"] } },
+ "default": "no"
+ }
+ ]
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/engines.json b/toolkit/components/search/tests/xpcshell/data/engines.json
new file mode 100644
index 0000000000..01bcfd1d05
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engines.json
@@ -0,0 +1,98 @@
+{
+ "data": [
+ {
+ "webExtension": {
+ "id": "engine@search.mozilla.org"
+ },
+ "orderHint": 10000,
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "excluded": { "locales": { "matches": ["gd"] } },
+ "default": "yes"
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "engine-pref@search.mozilla.org"
+ },
+ "orderHint": 7000,
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "excluded": { "locales": { "matches": ["de"] } },
+ "default": "no",
+ "defaultPrivate": "yes"
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "engine-rel-searchform-purpose@search.mozilla.org"
+ },
+ "orderHint": 6000,
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "excluded": { "locales": { "matches": ["de", "fr"] } },
+ "default": "no"
+ },
+ {
+ "included": { "locales": { "matches": ["gd"] } },
+ "orderHint": 9000
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "engine-chromeicon@search.mozilla.org"
+ },
+ "orderHint": 8000,
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "excluded": { "locales": { "matches": ["de", "fr"] } },
+ "default": "no"
+ },
+ {
+ "included": { "regions": ["ru"] },
+ "default": "no"
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "engine-resourceicon@search.mozilla.org"
+ },
+ "orderHint": 9000,
+ "appliesTo": [
+ {
+ "included": { "locales": { "matches": ["en-US", "fr"] } },
+ "excluded": { "regions": ["ru"] },
+ "default": "no"
+ },
+ {
+ "included": { "locales": { "matches": ["gd"] } },
+ "default": "yes",
+ "webExtension": {
+ "locales": ["gd"]
+ }
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "engine-reordered@search.mozilla.org"
+ },
+ "orderHint": 5000,
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "excluded": { "locales": { "matches": ["de", "fr"] } },
+ "default": "no"
+ }
+ ]
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/_locales/af/messages.json b/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/_locales/af/messages.json
new file mode 100644
index 0000000000..29ddd24df5
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/_locales/af/messages.json
@@ -0,0 +1,17 @@
+{
+ "extensionName": {
+ "message": "Multilocale"
+ },
+ "extensionDescription": {
+ "message": "Wikipedia, die vrye ensiklopedie"
+ },
+ "url_lang": {
+ "message": "af"
+ },
+ "searchUrl": {
+ "message": "https://af.wikipedia.org/wiki/Spesiaal:Soek"
+ },
+ "suggestUrl": {
+ "message": "https://af.wikipedia.org/w/api.php"
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/_locales/an/messages.json b/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/_locales/an/messages.json
new file mode 100644
index 0000000000..d21d910463
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/_locales/an/messages.json
@@ -0,0 +1,17 @@
+{
+ "extensionName": {
+ "message": "Multilocale"
+ },
+ "extensionDescription": {
+ "message": "A enciclopedia Libre"
+ },
+ "url_lang": {
+ "message": "an"
+ },
+ "searchUrl": {
+ "message": "https://an.wikipedia.org/wiki/Especial:Mirar"
+ },
+ "suggestUrl": {
+ "message": "https://an.wikipedia.org/w/api.php"
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/favicon.ico b/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/favicon.ico
new file mode 100644
index 0000000000..4314071e24
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/favicon.ico
Binary files differ
diff --git a/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/manifest.json b/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/manifest.json
new file mode 100644
index 0000000000..0fd835ca40
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/manifest.json
@@ -0,0 +1,23 @@
+{
+ "name": "__MSG_extensionName__",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "multilocale@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "description": "__MSG_extensionDescription__",
+ "icons": {
+ "16": "favicon.ico"
+ },
+ "default_locale": "af",
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "__MSG_extensionName__",
+ "search_url": "__MSG_searchUrl__",
+ "suggest_url": "__MSG_searchUrl__"
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/iconsRedirect.sjs b/toolkit/components/search/tests/xpcshell/data/iconsRedirect.sjs
new file mode 100644
index 0000000000..e08fb0b65a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/iconsRedirect.sjs
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Redirect a request for an icon to a different place, using a different
+ * content-type.
+ */
+
+function handleRequest(request, response) {
+ response.setStatusLine("1.1", 302, "Moved");
+ if (request.queryString == "type=invalid") {
+ response.setHeader("Content-Type", "image/png", false);
+ response.setHeader("Location", "engine.xml", false);
+ } else {
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Location", "remoteIcon.ico", false);
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/remoteIcon.ico b/toolkit/components/search/tests/xpcshell/data/remoteIcon.ico
new file mode 100644
index 0000000000..442ab4dc80
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/remoteIcon.ico
Binary files differ
diff --git a/toolkit/components/search/tests/xpcshell/data/search-legacy-correct-default-engine-hashes.json b/toolkit/components/search/tests/xpcshell/data/search-legacy-correct-default-engine-hashes.json
new file mode 100644
index 0000000000..e6091b7230
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/search-legacy-correct-default-engine-hashes.json
@@ -0,0 +1,112 @@
+{
+ "version": 1,
+ "buildID": "20121106",
+ "locale": "en-US",
+ "metaData": {
+ "current": "engine2",
+ "private": "engine2"
+ },
+ "engines": [
+ {
+ "_name": "engine1",
+ "_shortName": "engine1",
+ "_loadPath": "[other]addEngineWithDetails:engine1@search.mozilla.org",
+ "description": "A small test engine",
+ "__searchForm": null,
+ "_iconURL": "moz-extension://9c38b851-bede-2244-a086-9be8128dd64d/favicon.ico",
+ "_iconMapObj": {
+ "{}": "moz-extension://9c38b851-bede-2244-a086-9be8128dd64d/favicon.ico"
+ },
+ "_metaData": {
+ "alias": "testAlias"
+ },
+ "_urls": [
+ {
+ "template": "https://1.example.com/search",
+ "rels": [],
+ "resultDomain": "1.example.com",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ ],
+ "_isBuiltin": true,
+ "queryCharset": "UTF-8",
+ "extensionID": "engine1@search.mozilla.org"
+ },
+ {
+ "_name": "engine2",
+ "_shortName": "engine2",
+ "_loadPath": "[other]addEngineWithDetails:engine2@search.mozilla.org",
+ "description": "A small test engine",
+ "__searchForm": null,
+ "_iconURL": "moz-extension://0ea1d9b5-a14c-0e42-afaf-f25e8261c135/favicon.ico",
+ "_iconMapObj": {
+ "{}": "moz-extension://0ea1d9b5-a14c-0e42-afaf-f25e8261c135/favicon.ico"
+ },
+ "_metaData": {
+ "alias": null,
+ "hidden": false
+ },
+ "_urls": [
+ {
+ "template": "https://2.example.com/search",
+ "rels": [],
+ "resultDomain": "2.example.com",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ ],
+ "_isBuiltin": true,
+ "queryCharset": "UTF-8",
+ "extensionID": "engine2@search.mozilla.org"
+ },
+ {
+ "_name": "Test search engine",
+ "_shortName": "test-search-engine",
+ "description": "A test search engine (based on Google search)",
+ "__searchForm": "http://www.google.com/",
+ "_iconURL": "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA",
+ "_metaData": {},
+ "_urls": [
+ {
+ "template": "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}",
+ "rels": [],
+ "resultDomain": "suggestqueries.google.com",
+ "type": "application/x-suggestions+json",
+ "params": []
+ },
+ {
+ "template": "http://www.google.com/search",
+ "rels": [],
+ "resultDomain": "google.com",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "channel",
+ "value": "fflb",
+ "purpose": "keyword"
+ },
+ {
+ "name": "channel",
+ "value": "rcs",
+ "purpose": "contextmenu"
+ }
+ ]
+ }
+ ],
+ "queryCharset": "UTF-8",
+ "extensionID": "test-addon-id@mozilla.org"
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/search-legacy-no-ids.json b/toolkit/components/search/tests/xpcshell/data/search-legacy-no-ids.json
new file mode 100644
index 0000000000..733c323876
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/search-legacy-no-ids.json
@@ -0,0 +1,87 @@
+{
+ "version": 6,
+ "engines": [
+ { "_name": "Google", "_isAppProvided": true, "_metaData": { "order": 1 } },
+ {
+ "_name": "Wikipedia (en)",
+ "_isAppProvided": true,
+ "_metaData": { "order": 7 }
+ },
+ { "_name": "Bing", "_isAppProvided": true, "_metaData": { "order": 3 } },
+ {
+ "_name": "Amazon.co.uk",
+ "_isAppProvided": true,
+ "_metaData": { "order": 2 }
+ },
+ {
+ "_name": "DuckDuckGo",
+ "_isAppProvided": true,
+ "_metaData": { "order": 4 }
+ },
+ { "_name": "eBay", "_isAppProvided": true, "_metaData": { "order": 5 } },
+ {
+ "_name": "Policy",
+ "_loadPath": "[other]addEngineWithDetails:set-via-policy",
+ "_metaData": { "alias": "PolicyAlias", "order": 6 }
+ },
+ {
+ "_name": "Bugzilla@Mozilla",
+ "_loadPath": "[https]bugzilla.mozilla.org/bugzillamozilla.xml",
+ "description": "Bugzilla@Mozilla Quick Search",
+ "_metaData": {
+ "loadPathHash": "Bxz6jVe3IIBxLLaafUus536LMyLKoGZm7xsBv/yiTw8=",
+ "order": 8,
+ "alias": "bugzillaAlias"
+ },
+ "_urls": [
+ {
+ "params": [],
+ "rels": [],
+ "template": "https://bugzilla.mozilla.org/buglist.cgi?quicksearch={searchTerms}"
+ }
+ ],
+ "_orderHint": null,
+ "_telemetryId": null,
+ "_updateInterval": null,
+ "_updateURL": null,
+ "_iconUpdateURL": null,
+ "_extensionID": null,
+ "_locale": null,
+ "_definedAliases": []
+ },
+ {
+ "_name": "User",
+ "_loadPath": "[other]addEngineWithDetails:set-via-user",
+ "_metaData": {
+ "order": 9,
+ "alias": "UserAlias"
+ },
+ "_urls": [
+ {
+ "params": [],
+ "rels": [],
+ "template": "https://example.com/test?q={searchTerms}"
+ }
+ ],
+ "_telemetryId": null,
+ "_updateInterval": null,
+ "_updateURL": null,
+ "_iconUpdateURL": null,
+ "_extensionID": null,
+ "_locale": null,
+ "_hasPreferredIcon": null
+ },
+ { "_name": "Amazon.com", "_isAppProvided": true, "_metaData": {} }
+ ],
+ "metaData": {
+ "useSavedOrder": true,
+ "locale": "en-US",
+ "region": "GB",
+ "channel": "default",
+ "experiment": "",
+ "distroID": "",
+ "appDefaultEngine": "Google",
+ "current": "Bing",
+ "hash": "5Of6s1D+BDjPRti1wqtFyBTH1PnOf9n6cRwWlEXZhd0="
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/search-legacy-old-loadPaths.json b/toolkit/components/search/tests/xpcshell/data/search-legacy-old-loadPaths.json
new file mode 100644
index 0000000000..f716fb3e4a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/search-legacy-old-loadPaths.json
@@ -0,0 +1,135 @@
+{
+ "version": 7,
+ "engines": [
+ {
+ "id": "google@search.mozilla.orgdefault",
+ "_name": "Google",
+ "_isAppProvided": true,
+ "_metaData": { "order": 1 }
+ },
+ {
+ "id": "amazon@search.mozilla.orgen-GB",
+ "_name": "Amazon.co.uk",
+ "_isAppProvided": true,
+ "_metaData": { "order": 2 }
+ },
+ {
+ "id": "bing@search.mozilla.orgdefault",
+ "_name": "Bing",
+ "_isAppProvided": true,
+ "_metaData": { "order": 3 }
+ },
+ {
+ "id": "ddg@search.mozilla.orgdefault",
+ "_name": "DuckDuckGo",
+ "_isAppProvided": true,
+ "_metaData": { "order": 4 }
+ },
+ {
+ "id": "ebay@search.mozilla.orguk",
+ "_name": "eBay",
+ "_isAppProvided": true,
+ "_metaData": { "order": 5 }
+ },
+ {
+ "id": "wikipedia@search.mozilla.orgdefault",
+ "_name": "Wikipedia (en)",
+ "_isAppProvided": true,
+ "_metaData": { "order": 6 }
+ },
+ {
+ "id": "policy-Policy",
+ "_name": "Policy",
+ "_loadPath": "[other]addEngineWithDetails:set-via-policy",
+ "_metaData": { "alias": "PolicyAlias", "order": 6 }
+ },
+ {
+ "id": "bbc163e7-7b1a-47aa-a32c-c59062de2753",
+ "_name": "Bugzilla@Mozilla",
+ "_loadPath": "[https]bugzilla.mozilla.org/bugzillamozilla.xml",
+ "description": "Bugzilla@Mozilla Quick Search",
+ "_metaData": {
+ "loadPathHash": "Bxz6jVe3IIBxLLaafUus536LMyLKoGZm7xsBv/yiTw8=",
+ "order": 8,
+ "alias": "bugzillaAlias"
+ },
+ "_urls": [
+ {
+ "params": [],
+ "rels": [],
+ "template": "https://bugzilla.mozilla.org/buglist.cgi?quicksearch={searchTerms}"
+ }
+ ],
+ "_orderHint": null,
+ "_telemetryId": null,
+ "_updateInterval": null,
+ "_updateURL": null,
+ "_iconUpdateURL": null,
+ "_extensionID": null,
+ "_locale": null,
+ "_definedAliases": []
+ },
+ {
+ "id": "bbc163e7-7b1a-47aa-a32c-c59062de2754",
+ "_name": "User",
+ "_loadPath": "[other]addEngineWithDetails:set-via-user",
+ "_metaData": {
+ "order": 9,
+ "alias": "UserAlias"
+ },
+ "_urls": [
+ {
+ "params": [],
+ "rels": [],
+ "template": "https://example.com/test?q={searchTerms}"
+ }
+ ],
+ "_telemetryId": null,
+ "_updateInterval": null,
+ "_updateURL": null,
+ "_iconUpdateURL": null,
+ "_extensionID": null,
+ "_locale": null,
+ "_hasPreferredIcon": null
+ },
+ {
+ "id": "example@tests.mozilla.orgdefault",
+ "_name": "Example",
+ "_loadPath": "[other]addEngineWithDetails:example@tests.mozilla.org",
+ "description": null,
+ "_iconURL": "",
+ "_metaData": {},
+ "_urls": [
+ {
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ],
+ "rels": [],
+ "template": "https://example.com/"
+ }
+ ],
+ "_telemetryId": null,
+ "_updateInterval": null,
+ "_updateURL": null,
+ "_iconUpdateURL": null,
+ "_extensionID": "example@tests.mozilla.org",
+ "_locale": "default",
+ "_definedAliases": [],
+ "_hasPreferredIcon": null
+ }
+ ],
+ "metaData": {
+ "useSavedOrder": true,
+ "locale": "en-US",
+ "region": "GB",
+ "channel": "default",
+ "experiment": "",
+ "distroID": "",
+ "appDefaultEngine": "Google",
+ "current": "Bing",
+ "hash": "5Of6s1D+BDjPRti1wqtFyBTH1PnOf9n6cRwWlEXZhd0="
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/search-legacy-wrong-default-engine-hashes.json b/toolkit/components/search/tests/xpcshell/data/search-legacy-wrong-default-engine-hashes.json
new file mode 100644
index 0000000000..ca7081f565
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/search-legacy-wrong-default-engine-hashes.json
@@ -0,0 +1,114 @@
+{
+ "version": 1,
+ "buildID": "20121106",
+ "locale": "en-US",
+ "metaData": {
+ "current": "engine2",
+ "private": "engine2",
+ "hash": "wrong-hash-o/HzjHlVpb97AFGH3pY1GZ6CoTQkQslUKRd38/qasto=",
+ "privateHash": "wrong-hash-o/HzjHlVpb97AFGH3pY1GZ6CoTQkQslUKRd38/qasto="
+ },
+ "engines": [
+ {
+ "_name": "engine1",
+ "_shortName": "engine1",
+ "_loadPath": "[other]addEngineWithDetails:engine1@search.mozilla.org",
+ "description": "A small test engine",
+ "__searchForm": null,
+ "_iconURL": "moz-extension://9c38b851-bede-2244-a086-9be8128dd64d/favicon.ico",
+ "_iconMapObj": {
+ "{}": "moz-extension://9c38b851-bede-2244-a086-9be8128dd64d/favicon.ico"
+ },
+ "_metaData": {
+ "alias": "testAlias"
+ },
+ "_urls": [
+ {
+ "template": "https://1.example.com/search",
+ "rels": [],
+ "resultDomain": "1.example.com",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ ],
+ "_isBuiltin": true,
+ "queryCharset": "UTF-8",
+ "extensionID": "engine1@search.mozilla.org"
+ },
+ {
+ "_name": "engine2",
+ "_shortName": "engine2",
+ "_loadPath": "[other]addEngineWithDetails:engine2@search.mozilla.org",
+ "description": "A small test engine",
+ "__searchForm": null,
+ "_iconURL": "moz-extension://0ea1d9b5-a14c-0e42-afaf-f25e8261c135/favicon.ico",
+ "_iconMapObj": {
+ "{}": "moz-extension://0ea1d9b5-a14c-0e42-afaf-f25e8261c135/favicon.ico"
+ },
+ "_metaData": {
+ "alias": null,
+ "hidden": false
+ },
+ "_urls": [
+ {
+ "template": "https://2.example.com/search",
+ "rels": [],
+ "resultDomain": "2.example.com",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ ],
+ "_isBuiltin": true,
+ "queryCharset": "UTF-8",
+ "extensionID": "engine2@search.mozilla.org"
+ },
+ {
+ "_name": "Test search engine",
+ "_shortName": "test-search-engine",
+ "description": "A test search engine (based on Google search)",
+ "__searchForm": "http://www.google.com/",
+ "_iconURL": "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA",
+ "_metaData": {},
+ "_urls": [
+ {
+ "template": "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}",
+ "rels": [],
+ "resultDomain": "suggestqueries.google.com",
+ "type": "application/x-suggestions+json",
+ "params": []
+ },
+ {
+ "template": "http://www.google.com/search",
+ "rels": [],
+ "resultDomain": "google.com",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "channel",
+ "value": "fflb",
+ "purpose": "keyword"
+ },
+ {
+ "name": "channel",
+ "value": "rcs",
+ "purpose": "contextmenu"
+ }
+ ]
+ }
+ ],
+ "queryCharset": "UTF-8",
+ "extensionID": "test-addon-id@mozilla.org"
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/search-legacy-wrong-third-party-engine-hashes.json b/toolkit/components/search/tests/xpcshell/data/search-legacy-wrong-third-party-engine-hashes.json
new file mode 100644
index 0000000000..25329e083f
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/search-legacy-wrong-third-party-engine-hashes.json
@@ -0,0 +1,114 @@
+{
+ "version": 1,
+ "buildID": "20121106",
+ "locale": "en-US",
+ "metaData": {
+ "current": "Test search engine",
+ "private": "Test search engine",
+ "hash": "wrong-hash-o/HzjHlVpb97AFGH3pY1GZ6CoTQkQslUKRd38/qasto=",
+ "privateHash": "wrong-hash-o/HzjHlVpb97AFGH3pY1GZ6CoTQkQslUKRd38/qasto="
+ },
+ "engines": [
+ {
+ "_name": "engine1",
+ "_shortName": "engine1",
+ "_loadPath": "[other]addEngineWithDetails:engine1@search.mozilla.org",
+ "description": "A small test engine",
+ "__searchForm": null,
+ "_iconURL": "moz-extension://9c38b851-bede-2244-a086-9be8128dd64d/favicon.ico",
+ "_iconMapObj": {
+ "{}": "moz-extension://9c38b851-bede-2244-a086-9be8128dd64d/favicon.ico"
+ },
+ "_metaData": {
+ "alias": "testAlias"
+ },
+ "_urls": [
+ {
+ "template": "https://1.example.com/search",
+ "rels": [],
+ "resultDomain": "1.example.com",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ ],
+ "_isBuiltin": true,
+ "queryCharset": "UTF-8",
+ "extensionID": "engine1@search.mozilla.org"
+ },
+ {
+ "_name": "engine2",
+ "_shortName": "engine2",
+ "_loadPath": "[other]addEngineWithDetails:engine2@search.mozilla.org",
+ "description": "A small test engine",
+ "__searchForm": null,
+ "_iconURL": "moz-extension://0ea1d9b5-a14c-0e42-afaf-f25e8261c135/favicon.ico",
+ "_iconMapObj": {
+ "{}": "moz-extension://0ea1d9b5-a14c-0e42-afaf-f25e8261c135/favicon.ico"
+ },
+ "_metaData": {
+ "alias": null,
+ "hidden": false
+ },
+ "_urls": [
+ {
+ "template": "https://2.example.com/search",
+ "rels": [],
+ "resultDomain": "2.example.com",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ ],
+ "_isBuiltin": true,
+ "queryCharset": "UTF-8",
+ "extensionID": "engine2@search.mozilla.org"
+ },
+ {
+ "_name": "Test search engine",
+ "_shortName": "test-search-engine",
+ "description": "A test search engine (based on Google search)",
+ "__searchForm": "http://www.google.com/",
+ "_iconURL": "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA",
+ "_metaData": {},
+ "_urls": [
+ {
+ "template": "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}",
+ "rels": [],
+ "resultDomain": "suggestqueries.google.com",
+ "type": "application/x-suggestions+json",
+ "params": []
+ },
+ {
+ "template": "http://www.google.com/search",
+ "rels": [],
+ "resultDomain": "google.com",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "channel",
+ "value": "fflb",
+ "purpose": "keyword"
+ },
+ {
+ "name": "channel",
+ "value": "rcs",
+ "purpose": "contextmenu"
+ }
+ ]
+ }
+ ],
+ "queryCharset": "UTF-8",
+ "extensionID": "test-addon-id@mozilla.org"
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/search-legacy.json b/toolkit/components/search/tests/xpcshell/data/search-legacy.json
new file mode 100644
index 0000000000..c8416f3813
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/search-legacy.json
@@ -0,0 +1,109 @@
+{
+ "version": 1,
+ "buildID": "20121106",
+ "locale": "en-US",
+ "metaData": {},
+ "engines": [
+ {
+ "_name": "engine1",
+ "_shortName": "engine1",
+ "_loadPath": "[other]addEngineWithDetails:engine1@search.mozilla.org",
+ "description": "A small test engine",
+ "__searchForm": null,
+ "_iconURL": "moz-extension://9c38b851-bede-2244-a086-9be8128dd64d/favicon.ico",
+ "_iconMapObj": {
+ "{}": "moz-extension://9c38b851-bede-2244-a086-9be8128dd64d/favicon.ico"
+ },
+ "_metaData": {
+ "alias": "testAlias"
+ },
+ "_urls": [
+ {
+ "template": "https://1.example.com/search",
+ "rels": [],
+ "resultDomain": "1.example.com",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ ],
+ "_isBuiltin": true,
+ "queryCharset": "UTF-8",
+ "extensionID": "engine1@search.mozilla.org"
+ },
+ {
+ "_name": "engine2",
+ "_shortName": "engine2",
+ "_loadPath": "[other]addEngineWithDetails:engine2@search.mozilla.org",
+ "description": "A small test engine",
+ "__searchForm": null,
+ "_iconURL": "moz-extension://0ea1d9b5-a14c-0e42-afaf-f25e8261c135/favicon.ico",
+ "_iconMapObj": {
+ "{}": "moz-extension://0ea1d9b5-a14c-0e42-afaf-f25e8261c135/favicon.ico"
+ },
+ "_metaData": {
+ "alias": null,
+ "hidden": true
+ },
+ "_urls": [
+ {
+ "template": "https://2.example.com/search",
+ "rels": [],
+ "resultDomain": "2.example.com",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ ],
+ "_isBuiltin": true,
+ "queryCharset": "UTF-8",
+ "extensionID": "engine2@search.mozilla.org"
+ },
+ {
+ "_name": "Test search engine",
+ "_shortName": "test-search-engine",
+ "description": "A test search engine (based on Google search)",
+ "__searchForm": "http://www.google.com/",
+ "_iconURL": "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA",
+ "_metaData": {},
+ "_urls": [
+ {
+ "template": "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}",
+ "rels": [],
+ "resultDomain": "suggestqueries.google.com",
+ "type": "application/x-suggestions+json",
+ "params": []
+ },
+ {
+ "template": "http://www.google.com/search",
+ "rels": [],
+ "resultDomain": "google.com",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "channel",
+ "value": "fflb",
+ "purpose": "keyword"
+ },
+ {
+ "name": "channel",
+ "value": "rcs",
+ "purpose": "contextmenu"
+ }
+ ]
+ }
+ ],
+ "queryCharset": "UTF-8",
+ "extensionID": "test-addon-id@mozilla.org"
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/search-migration.json b/toolkit/components/search/tests/xpcshell/data/search-migration.json
new file mode 100644
index 0000000000..520149a370
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/search-migration.json
@@ -0,0 +1,68 @@
+{
+ "version": 1,
+ "buildID": "20121106",
+ "locale": "en-US",
+ "metaData": {},
+ "engines": [
+ {
+ "_name": "engine1",
+ "_metaData": {
+ "alias": "testAlias"
+ },
+ "_isAppProvided": true
+ },
+ {
+ "_name": "engine2",
+ "_metaData": {
+ "alias": null,
+ "hidden": true
+ },
+ "_isAppProvided": true
+ },
+ {
+ "_name": "simple",
+ "_loadPath": "jar:[profile]/extensions/simple@tests.mozilla.org.xpi!/simple.xml",
+ "_shortName": "simple",
+ "description": "A migration test engine",
+ "__searchForm": "http://www.example.com/",
+ "_metaData": {},
+ "_urls": [
+ {
+ "template": "http://www.example.com/search",
+ "rels": [],
+ "resultDomain": "google.com",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ ],
+ "queryCharset": "UTF-8"
+ },
+ {
+ "_name": "simple search",
+ "_loadPath": "[other]addEngineWithDetails:simple@tests.mozilla.org",
+ "_shortName": "simple search",
+ "description": "A migration test engine",
+ "__searchForm": "http://www.example.com/",
+ "_metaData": {},
+ "_urls": [
+ {
+ "template": "http://www.example.com/search",
+ "rels": [],
+ "resultDomain": "google.com",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ ],
+ "queryCharset": "UTF-8",
+ "_extensionID": "simple@tests.mozilla.org"
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/search-obsolete-app.json b/toolkit/components/search/tests/xpcshell/data/search-obsolete-app.json
new file mode 100644
index 0000000000..151359ff73
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/search-obsolete-app.json
@@ -0,0 +1,41 @@
+{
+ "version": 1,
+ "buildID": "20121106",
+ "locale": "en-US",
+ "metaData": {},
+ "engines": [
+ {
+ "_name": "engine1",
+ "_metaData": {
+ "alias": "testAlias"
+ },
+ "_isAppProvided": true
+ },
+ {
+ "_name": "engine2",
+ "_metaData": {
+ "alias": null,
+ "hidden": true
+ },
+ "_isAppProvided": true
+ },
+ {
+ "_name": "App",
+ "_shortName": "app",
+ "_loadPath": "jar:[app]/omni.ja!distribution.xml",
+ "description": "App Search",
+ "__searchForm": null,
+ "_metaData": {},
+ "_urls": [
+ {
+ "template": "https://example.com/search",
+ "rels": ["searchform"],
+ "resultDomain": "example.com",
+ "params": []
+ }
+ ],
+ "queryCharset": "UTF-8",
+ "_readOnly": false
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/search-obsolete-distribution.json b/toolkit/components/search/tests/xpcshell/data/search-obsolete-distribution.json
new file mode 100644
index 0000000000..efc609a5af
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/search-obsolete-distribution.json
@@ -0,0 +1,41 @@
+{
+ "version": 1,
+ "buildID": "20121106",
+ "locale": "en-US",
+ "metaData": {},
+ "engines": [
+ {
+ "_name": "engine1",
+ "_metaData": {
+ "alias": "testAlias"
+ },
+ "_isAppProvided": true
+ },
+ {
+ "_name": "engine2",
+ "_metaData": {
+ "alias": null,
+ "hidden": true
+ },
+ "_isAppProvided": true
+ },
+ {
+ "_name": "Distribution",
+ "_shortName": "distribution",
+ "_loadPath": "[distribution]/searchplugins/common/distribution.xml",
+ "description": "Distribution Search",
+ "__searchForm": null,
+ "_metaData": {},
+ "_urls": [
+ {
+ "template": "https://example.com/search",
+ "rels": ["searchform"],
+ "resultDomain": "example.com",
+ "params": []
+ }
+ ],
+ "queryCharset": "UTF-8",
+ "_readOnly": false
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/search-obsolete-langpack.json b/toolkit/components/search/tests/xpcshell/data/search-obsolete-langpack.json
new file mode 100644
index 0000000000..8c45b4d61f
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/search-obsolete-langpack.json
@@ -0,0 +1,91 @@
+{
+ "version": 1,
+ "buildID": "20121106",
+ "locale": "en-US",
+ "metaData": {},
+ "engines": [
+ {
+ "_name": "engine1",
+ "_metaData": {
+ "alias": "testAlias"
+ },
+ "_isAppProvided": true
+ },
+ {
+ "_name": "engine2",
+ "_metaData": {
+ "alias": null,
+ "hidden": true
+ },
+ "_isAppProvided": true
+ },
+ {
+ "_name": "Langpack",
+ "_shortName": "langpack-ru",
+ "_loadPath": "jar:[app]/extensions/langpack-ru@firefox.mozilla.org.xpi!browser/langpack.xml",
+ "description": "Langpack search",
+ "__searchForm": null,
+ "_metaData": {},
+ "_urls": [
+ {
+ "template": "https://example.com/search",
+ "rels": ["searchform"],
+ "resultDomain": "example.com",
+ "params": []
+ }
+ ],
+ "queryCharset": "UTF-8"
+ },
+ {
+ "_name": "Langpack1",
+ "_shortName": "langpack1-ru",
+ "_loadPath": "[app]/extensions/langpack-ru@firefox.mozilla.org.xpi!browser/langpack1.xml",
+ "description": "Langpack1 search",
+ "__searchForm": null,
+ "_metaData": {},
+ "_urls": [
+ {
+ "template": "https://example1.com/search",
+ "rels": ["searchform"],
+ "resultDomain": "example1.com",
+ "params": []
+ }
+ ],
+ "queryCharset": "UTF-8"
+ },
+ {
+ "_name": "Langpack2",
+ "_shortName": "langpack2-ru",
+ "_loadPath": "jar:[profile]/extensions/langpack-ru@firefox.mozilla.org.xpi!browser/langpack2.xml",
+ "description": "Langpack2 search",
+ "__searchForm": null,
+ "_metaData": {},
+ "_urls": [
+ {
+ "template": "https://example2.com/search",
+ "rels": ["searchform"],
+ "resultDomain": "example2.com",
+ "params": []
+ }
+ ],
+ "queryCharset": "UTF-8"
+ },
+ {
+ "_name": "Langpack3",
+ "_shortName": "langpack3-ru",
+ "_loadPath": "jar:[other]/langpack-ru@firefox.mozilla.org.xpi!browser/langpack3.xml",
+ "description": "Langpack3 search",
+ "__searchForm": null,
+ "_metaData": {},
+ "_urls": [
+ {
+ "template": "https://example3.com/search",
+ "rels": ["searchform"],
+ "resultDomain": "example3.com",
+ "params": []
+ }
+ ],
+ "queryCharset": "UTF-8"
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/search.json b/toolkit/components/search/tests/xpcshell/data/search.json
new file mode 100644
index 0000000000..79d1368687
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/search.json
@@ -0,0 +1,72 @@
+{
+ "version": 1,
+ "buildID": "20121106",
+ "locale": "en-US",
+ "metaData": {},
+ "engines": [
+ {
+ "id": "engine1@search.mozilla.orgdefault",
+ "_name": "engine1",
+ "_metaData": {
+ "alias": "testAlias"
+ },
+ "_isAppProvided": true
+ },
+ {
+ "id": "engine2@search.mozilla.orgdefault",
+ "_name": "engine2",
+ "_metaData": {
+ "alias": null,
+ "hidden": true
+ },
+ "_isAppProvided": true
+ },
+ {
+ "id": "test-addon-id@mozilla.orgdefault",
+ "_name": "Test search engine",
+ "_shortName": "test-search-engine",
+ "description": "A test search engine (based on Google search)",
+ "__searchForm": "http://www.google.com/",
+ "_iconURL": "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA",
+ "_metaData": {},
+ "_urls": [
+ {
+ "template": "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}",
+ "rels": [],
+ "resultDomain": "suggestqueries.google.com",
+ "type": "application/x-suggestions+json",
+ "params": []
+ },
+ {
+ "template": "http://www.google.com/search",
+ "rels": [],
+ "resultDomain": "google.com",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "channel",
+ "value": "fflb",
+ "purpose": "searchbar"
+ },
+ {
+ "name": "channel",
+ "value": "rcs",
+ "purpose": "contextmenu"
+ },
+ {
+ "name": "myparam",
+ "mozparam": true,
+ "condition": "pref",
+ "pref": "test"
+ }
+ ]
+ }
+ ],
+ "queryCharset": "UTF-8",
+ "_extensionID": "test-addon-id@mozilla.org"
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/searchSuggestions.sjs b/toolkit/components/search/tests/xpcshell/data/searchSuggestions.sjs
new file mode 100644
index 0000000000..85f51795b6
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/searchSuggestions.sjs
@@ -0,0 +1,181 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+let { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+Cu.importGlobalProperties(["TextEncoder"]);
+
+/**
+ * Provide search suggestions in the OpenSearch JSON format.
+ */
+
+function handleRequest(request, response) {
+ // Get the query parameters from the query string.
+ let query = parseQueryString(request.queryString);
+
+ function convertToUtf8(str) {
+ return String.fromCharCode(...new TextEncoder().encode(str));
+ }
+
+ function writeSuggestions(q, completions = []) {
+ let jsonString = JSON.stringify([q, completions]);
+
+ // This script must be evaluated as UTF-8 for this to write out the bytes of
+ // the string in UTF-8. If it's evaluated as Latin-1, the written bytes
+ // will be the result of UTF-8-encoding the result-string *twice*, which
+ // will break the "I ❤️" case further down.
+ let stringOfUtf8Bytes = convertToUtf8(jsonString);
+
+ response.write(stringOfUtf8Bytes);
+ }
+
+ /**
+ * Sends `data` as suggestions directly. This is useful when testing rich
+ * suggestions, which do not conform to the object shape sent by
+ * writeSuggestions.
+ *
+ * @param {Array} data The data to send as suggestions.
+ */
+ function writeSuggestionsDirectly(data) {
+ let jsonString = JSON.stringify(data);
+ let stringOfUtf8Bytes = convertToUtf8(jsonString);
+ response.setHeader("Content-Type", "application/json", false);
+ response.write(stringOfUtf8Bytes);
+ }
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ let q = request.method == "GET" ? query.q : undefined;
+ if (q == "cookie") {
+ response.setHeader("Set-Cookie", "cookie=1");
+ writeSuggestions(q);
+ } else if (q == "no remote" || q == "no results") {
+ writeSuggestions(q);
+ } else if (q == "Query Mismatch") {
+ writeSuggestions("This is an incorrect query string", ["some result"]);
+ } else if (q == "Query Case Mismatch") {
+ writeSuggestions(q.toUpperCase(), [q]);
+ } else if (q == "") {
+ writeSuggestions("", ["The server should never be sent an empty query"]);
+ } else if (q && q.startsWith("mo")) {
+ writeSuggestions(q, ["Mozilla", "modern", "mom"]);
+ } else if (q && q.startsWith("I ❤️")) {
+ writeSuggestions(q, ["I ❤️ Mozilla"]);
+ } else if (q && q.startsWith("stü")) {
+ writeSuggestions("st\\u00FC", ["stühle", "stüssy"]);
+ } else if (q && q.startsWith("tailjunk ")) {
+ writeSuggestionsDirectly([
+ q,
+ [q + " normal", q + " tail 1", q + " tail 2"],
+ [],
+ {
+ "google:irrelevantparameter": [],
+ "google:badformat": {
+ "google:suggestdetail": [
+ {},
+ { mp: "… ", t: "tail 1" },
+ { mp: "… ", t: "tail 2" },
+ ],
+ },
+ },
+ ]);
+ } else if (q && q.startsWith("tailjunk few ")) {
+ writeSuggestionsDirectly([
+ q,
+ [q + " normal", q + " tail 1", q + " tail 2"],
+ [],
+ {
+ "google:irrelevantparameter": [],
+ "google:badformat": {
+ "google:suggestdetail": [{ mp: "… ", t: "tail 1" }],
+ },
+ },
+ ]);
+ } else if (q && q.startsWith("tailalt ")) {
+ writeSuggestionsDirectly([
+ q,
+ [q + " normal", q + " tail 1", q + " tail 2"],
+ {
+ "google:suggestdetail": [
+ {},
+ { mp: "… ", t: "tail 1" },
+ { mp: "… ", t: "tail 2" },
+ ],
+ },
+ ]);
+ } else if (q && q.startsWith("tail ")) {
+ writeSuggestionsDirectly([
+ q,
+ [q + " normal", q + " tail 1", q + " tail 2"],
+ [],
+ {
+ "google:irrelevantparameter": [],
+ "google:suggestdetail": [
+ {},
+ { mp: "… ", t: "tail 1" },
+ { mp: "… ", t: "tail 2" },
+ ],
+ },
+ ]);
+ } else if (q && q.startsWith("richempty ")) {
+ writeSuggestionsDirectly([
+ q,
+ [q + " normal", q + " tail 1", q + " tail 2"],
+ [],
+ {
+ "google:irrelevantparameter": [],
+ "google:suggestdetail": [],
+ },
+ ]);
+ } else if (q && q.startsWith("letter ")) {
+ let letters = [];
+ for (
+ let charCode = "A".charCodeAt();
+ charCode <= "Z".charCodeAt();
+ charCode++
+ ) {
+ letters.push("letter " + String.fromCharCode(charCode));
+ }
+ writeSuggestions(q, letters);
+ } else if (q && q.startsWith("HTTP ")) {
+ response.setStatusLine(request.httpVersion, q.replace("HTTP ", ""), q);
+ writeSuggestions(q, [q]);
+ } else if (q && q.startsWith("delay")) {
+ // Delay the response by delayMs milliseconds. 200ms is the default, less
+ // than the timeout but hopefully enough to abort before completion.
+ let match = /^delay([0-9]+)/.exec(q);
+ let delayMs = match ? parseInt(match[1]) : 200;
+ response.processAsync();
+ writeSuggestions(q, [q]);
+ setTimeout(() => response.finish(), delayMs);
+ } else if (q && q.startsWith("slow ")) {
+ // Delay the response by 10 seconds so the client timeout is reached.
+ response.processAsync();
+ writeSuggestions(q, [q]);
+ setTimeout(() => response.finish(), 10000);
+ } else if (request.method == "POST") {
+ // This includes headers, not just the body
+ let requestText = NetUtil.readInputStreamToString(
+ request.bodyInputStream,
+ request.bodyInputStream.available()
+ );
+ // Only use the last line which contains the encoded params
+ let requestLines = requestText.split("\n");
+ let postParams = parseQueryString(requestLines[requestLines.length - 1]);
+ writeSuggestions(postParams.q, ["Mozilla", "modern", "mom"]);
+ } else {
+ response.setStatusLine(request.httpVersion, 404, "Not Found");
+ }
+}
+
+function parseQueryString(queryString) {
+ let query = {};
+ queryString.split("&").forEach(function (val) {
+ let [name, value] = val.split("=");
+ query[name] = decodeURIComponent(value).replace(/[+]/g, " ");
+ });
+ return query;
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/search_ignorelist.json b/toolkit/components/search/tests/xpcshell/data/search_ignorelist.json
new file mode 100644
index 0000000000..35240893ec
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/search_ignorelist.json
@@ -0,0 +1,51 @@
+{
+ "version": 1,
+ "buildID": "20121106",
+ "locale": "en-US",
+ "metaData": {},
+ "engines": [
+ {
+ "_name": "Test search engine",
+ "_shortName": "test-search-engine",
+ "description": "A test search engine (based on Google search)",
+ "extensionID": "test-addon-id@mozilla.org",
+ "__searchForm": "http://www.google.com/",
+ "_iconURL": "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA",
+ "_metaData": {},
+ "_urls": [
+ {
+ "template": "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}",
+ "rels": [],
+ "type": "application/x-suggestions+json",
+ "params": []
+ },
+ {
+ "template": "http://www.google.com/search",
+ "resultDomain": "google.com",
+ "rels": [],
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "ignore",
+ "value": "true"
+ },
+ {
+ "name": "channel",
+ "value": "fflb",
+ "purpose": "keyword"
+ },
+ {
+ "name": "channel",
+ "value": "rcs",
+ "purpose": "contextmenu"
+ }
+ ]
+ }
+ ],
+ "queryCharset": "UTF-8"
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/svgIcon.svg b/toolkit/components/search/tests/xpcshell/data/svgIcon.svg
new file mode 100644
index 0000000000..e2550f8d5d
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/svgIcon.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="16" height="16" viewBox="0 0 16 16">
+ <rect x="4" y="4" width="8px" height="8px" style="fill: blue" />
+</svg>
diff --git a/toolkit/components/search/tests/xpcshell/data1/engine1/manifest.json b/toolkit/components/search/tests/xpcshell/data1/engine1/manifest.json
new file mode 100644
index 0000000000..5fa44ea692
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data1/engine1/manifest.json
@@ -0,0 +1,27 @@
+{
+ "name": "engine1",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine1@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "description": "A small test engine",
+ "icons": {
+ "16": "favicon.ico"
+ },
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "engine1",
+ "search_url": "https://1.example.com/search",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data1/engine2/manifest.json b/toolkit/components/search/tests/xpcshell/data1/engine2/manifest.json
new file mode 100644
index 0000000000..7ab094198b
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data1/engine2/manifest.json
@@ -0,0 +1,27 @@
+{
+ "name": "engine2",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine2@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "description": "A small test engine",
+ "icons": {
+ "16": "favicon.ico"
+ },
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "engine2",
+ "search_url": "https://2.example.com/search",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data1/engines.json b/toolkit/components/search/tests/xpcshell/data1/engines.json
new file mode 100644
index 0000000000..05556ef87c
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data1/engines.json
@@ -0,0 +1,60 @@
+{
+ "data": [
+ {
+ "webExtension": {
+ "id": "engine1@search.mozilla.org"
+ },
+ "orderHint": 10000,
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "default": "yes-if-no-other",
+ "defaultPrivate": "yes-if-no-other"
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "engine2@search.mozilla.org"
+ },
+ "orderHint": 7000,
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "default": "no"
+ },
+ {
+ "included": { "everywhere": true },
+ "default": "yes",
+ "experiment": "exp1"
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "exp2@search.mozilla.org"
+ },
+ "orderHint": 5000,
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "defaultPrivate": "yes",
+ "experiment": "exp2"
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "exp3@search.mozilla.org"
+ },
+ "orderHint": 20000,
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "default": "yes",
+ "experiment": "exp3"
+ }
+ ]
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/data1/exp2/manifest.json b/toolkit/components/search/tests/xpcshell/data1/exp2/manifest.json
new file mode 100644
index 0000000000..0cd0e080b9
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data1/exp2/manifest.json
@@ -0,0 +1,27 @@
+{
+ "name": "exp2",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "exp2@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "description": "A small test engine",
+ "icons": {
+ "16": "favicon.ico"
+ },
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "exp2",
+ "search_url": "https://2.example.com/searchexp",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data1/exp3/manifest.json b/toolkit/components/search/tests/xpcshell/data1/exp3/manifest.json
new file mode 100644
index 0000000000..4e023e0fef
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data1/exp3/manifest.json
@@ -0,0 +1,27 @@
+{
+ "name": "exp3",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "exp3@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "description": "A small test engine",
+ "icons": {
+ "16": "favicon.ico"
+ },
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "exp3",
+ "search_url": "https://3.example.com/searchexp",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/head_search.js b/toolkit/components/search/tests/xpcshell/head_search.js
new file mode 100644
index 0000000000..749fd2f4ad
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/head_search.js
@@ -0,0 +1,512 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
+ Region: "resource://gre/modules/Region.sys.mjs",
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+ RemoteSettingsClient:
+ "resource://services-settings/RemoteSettingsClient.sys.mjs",
+ SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs",
+ SearchService: "resource://gre/modules/SearchService.sys.mjs",
+ SearchSettings: "resource://gre/modules/SearchSettings.sys.mjs",
+ SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs",
+ SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+ clearTimeout: "resource://gre/modules/Timer.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+var { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+SearchTestUtils.init(this);
+
+const SETTINGS_FILENAME = "search.json.mozlz4";
+
+// nsSearchService.js uses Services.appinfo.name to build a salt for a hash.
+// eslint-disable-next-line mozilla/use-services
+var XULRuntime = Cc["@mozilla.org/xre/runtime;1"].getService(Ci.nsIXULRuntime);
+
+// Expand the amount of information available in error logs
+Services.prefs.setBoolPref("browser.search.log", true);
+Services.prefs.setBoolPref("browser.region.log", true);
+
+AddonTestUtils.init(this, false);
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+// Allow telemetry probes which may otherwise be disabled for some applications (e.g. Thunderbird)
+Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+);
+
+// For tests, allow the settings to write sooner than it would do normally so that
+// the tests that need to wait for it can run a bit faster.
+SearchSettings.SETTNGS_INVALIDATION_DELAY = 250;
+
+async function promiseSettingsData() {
+ let path = PathUtils.join(PathUtils.profileDir, SETTINGS_FILENAME);
+ return IOUtils.readJSON(path, { decompress: true });
+}
+
+function promiseSaveSettingsData(data) {
+ return IOUtils.writeJSON(
+ PathUtils.join(PathUtils.profileDir, SETTINGS_FILENAME),
+ data,
+ { compress: true }
+ );
+}
+
+async function promiseEngineMetadata() {
+ let settings = await promiseSettingsData();
+ let data = {};
+ for (let engine of settings.engines) {
+ data[engine._name] = engine._metaData;
+ }
+ return data;
+}
+
+async function promiseGlobalMetadata() {
+ return (await promiseSettingsData()).metaData;
+}
+
+async function promiseSaveGlobalMetadata(globalData) {
+ let data = await promiseSettingsData();
+ data.metaData = globalData;
+ await promiseSaveSettingsData(data);
+}
+
+function promiseDefaultNotification(type = "normal") {
+ return SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE[
+ type == "private" ? "DEFAULT_PRIVATE" : "DEFAULT"
+ ],
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+}
+
+/**
+ * Clean the profile of any settings file left from a previous run.
+ *
+ * @returns {boolean}
+ * Indicates if the settings file existed.
+ */
+function removeSettingsFile() {
+ let file = do_get_profile().clone();
+ file.append(SETTINGS_FILENAME);
+ if (file.exists()) {
+ file.remove(false);
+ return true;
+ }
+ return false;
+}
+
+/**
+ * isUSTimezone taken from nsSearchService.js
+ *
+ * @returns {boolean}
+ */
+function isUSTimezone() {
+ // Timezone assumptions! We assume that if the system clock's timezone is
+ // between Newfoundland and Hawaii, that the user is in North America.
+
+ // This includes all of South America as well, but we have relatively few
+ // en-US users there, so that's OK.
+
+ // 150 minutes = 2.5 hours (UTC-2.5), which is
+ // Newfoundland Daylight Time (http://www.timeanddate.com/time/zones/ndt)
+
+ // 600 minutes = 10 hours (UTC-10), which is
+ // Hawaii-Aleutian Standard Time (http://www.timeanddate.com/time/zones/hast)
+
+ let UTCOffset = new Date().getTimezoneOffset();
+ return UTCOffset >= 150 && UTCOffset <= 600;
+}
+
+const kTestEngineName = "Test search engine";
+
+/**
+ * Waits for the settings file to be saved.
+ *
+ * @returns {Promise} Resolved when the settings file is saved.
+ */
+function promiseAfterSettings() {
+ return SearchTestUtils.promiseSearchNotification(
+ "write-settings-to-disk-complete"
+ );
+}
+
+/**
+ * Sets the home region, and waits for the search service to reload the engines.
+ *
+ * @param {string} region
+ * The region to set.
+ */
+async function promiseSetHomeRegion(region) {
+ let promise = SearchTestUtils.promiseSearchNotification("engines-reloaded");
+ Region._setHomeRegion(region);
+ await promise;
+}
+
+/**
+ * Sets the requested/available locales and waits for the search service to
+ * reload the engines.
+ *
+ * @param {string} locale
+ * The locale to set.
+ */
+async function promiseSetLocale(locale) {
+ if (!Services.locale.availableLocales.includes(locale)) {
+ throw new Error(
+ `"${locale}" needs to be included in Services.locales.availableLocales at the start of the test.`
+ );
+ }
+
+ let promise = SearchTestUtils.promiseSearchNotification("engines-reloaded");
+ Services.locale.requestedLocales = [locale];
+ await promise;
+}
+
+/**
+ * Read a JSON file and return the JS object
+ *
+ * @param {nsIFile} file
+ * The file to read.
+ * @returns {object}
+ * Returns the JSON object if the file was successfully read,
+ * false otherwise.
+ */
+async function readJSONFile(file) {
+ return JSON.parse(await IOUtils.readUTF8(file.path));
+}
+
+/**
+ * Recursively compare two objects and check that every property of expectedObj has the same value
+ * on actualObj.
+ *
+ * @param {object} expectedObj
+ * The source object that we expect to match
+ * @param {object} actualObj
+ * The object to check against the source
+ * @param {Function} skipProp
+ * A function that is called with the property name and its value, to see if
+ * testing that property should be skipped or not.
+ */
+function isSubObjectOf(expectedObj, actualObj, skipProp) {
+ for (let prop in expectedObj) {
+ if (skipProp && skipProp(prop, expectedObj[prop])) {
+ continue;
+ }
+ if (expectedObj[prop] instanceof Object) {
+ Assert.equal(
+ actualObj[prop]?.length,
+ expectedObj[prop].length,
+ `Should have the correct length for property ${prop}`
+ );
+ isSubObjectOf(expectedObj[prop], actualObj[prop], skipProp);
+ } else {
+ Assert.equal(
+ actualObj[prop],
+ expectedObj[prop],
+ `Should have the correct value for property ${prop}`
+ );
+ }
+ }
+}
+
+/**
+ * After useHttpServer() is called, this string contains the URL of the "data"
+ * directory, including the final slash.
+ */
+var gDataUrl;
+
+/**
+ * Initializes the HTTP server and ensures that it is terminated when tests end.
+ *
+ * @param {string} dir
+ * The test sub-directory to use for the engines.
+ * @returns {HttpServer}
+ * The HttpServer object in case further customization is needed.
+ */
+function useHttpServer(dir = "data") {
+ let httpServer = new HttpServer();
+ httpServer.start(-1);
+ httpServer.registerDirectory("/", do_get_cwd());
+ gDataUrl = `http://localhost:${httpServer.identity.primaryPort}/${dir}/`;
+ registerCleanupFunction(async function cleanup_httpServer() {
+ await new Promise(resolve => {
+ httpServer.stop(resolve);
+ });
+ });
+ return httpServer;
+}
+
+// This "enum" from nsSearchService.js
+const TELEMETRY_RESULT_ENUM = {
+ SUCCESS: 0,
+ SUCCESS_WITHOUT_DATA: 1,
+ TIMEOUT: 2,
+ ERROR: 3,
+};
+
+/**
+ * Checks the value of the SEARCH_SERVICE_COUNTRY_FETCH_RESULT probe.
+ *
+ * @param {string|null} aExpectedValue
+ * If a value from TELEMETRY_RESULT_ENUM, we expect to see this value
+ * recorded exactly once in the probe. If |null|, we expect to see
+ * nothing recorded in the probe at all.
+ */
+function checkCountryResultTelemetry(aExpectedValue) {
+ let histogram = Services.telemetry.getHistogramById(
+ "SEARCH_SERVICE_COUNTRY_FETCH_RESULT"
+ );
+ let snapshot = histogram.snapshot();
+ if (aExpectedValue != null) {
+ equal(snapshot.values[aExpectedValue], 1);
+ } else {
+ deepEqual(snapshot.values, {});
+ }
+}
+
+/**
+ * Provides a basic set of remote settings for use in tests.
+ */
+async function setupRemoteSettings() {
+ const settings = await RemoteSettings("hijack-blocklists");
+ sinon.stub(settings, "get").returns([
+ {
+ id: "load-paths",
+ matches: ["[addon]searchignore@mozilla.com"],
+ _status: "synced",
+ },
+ {
+ id: "submission-urls",
+ matches: ["ignore=true"],
+ _status: "synced",
+ },
+ ]);
+}
+
+/**
+ * Helper function that sets up a server and respnds to region
+ * fetch requests.
+ *
+ * @param {string} region
+ * The region that the server will respond with.
+ * @param {Promise|null} waitToRespond
+ * A promise that the server will await on to delay responding
+ * to the request.
+ */
+function useCustomGeoServer(region, waitToRespond = Promise.resolve()) {
+ let srv = useHttpServer();
+ srv.registerPathHandler("/fetch_region", async (req, res) => {
+ res.processAsync();
+ await waitToRespond;
+ res.setStatusLine("1.1", 200, "OK");
+ res.write(JSON.stringify({ country_code: region }));
+ res.finish();
+ });
+
+ Services.prefs.setCharPref(
+ "browser.region.network.url",
+ `http://localhost:${srv.identity.primaryPort}/fetch_region`
+ );
+}
+
+/**
+ * @typedef {object} TelemetryDetails
+ * @property {string} engineId
+ * The telemetry ID for the search engine.
+ * @property {string} [displayName]
+ * The search engine's display name.
+ * @property {string} [loadPath]
+ * The load path for the search engine.
+ * @property {string} [submissionUrl]
+ * The submission URL for the search engine.
+ * @property {string} [verified]
+ * Whether the search engine is verified.
+ */
+
+/**
+ * Asserts that default search engine telemetry has been correctly reported
+ * to Glean.
+ *
+ * @param {object} expected
+ * An object containing telemetry details for normal and private engines.
+ * @param {TelemetryDetails} expected.normal
+ * An object with the expected details for the normal search engine.
+ * @param {TelemetryDetails} [expected.private]
+ * An object with the expected details for the private search engine.
+ */
+async function assertGleanDefaultEngine(expected) {
+ await TestUtils.waitForCondition(
+ () =>
+ Glean.searchEngineDefault.engineId.testGetValue() ==
+ (expected.normal.engineId ?? ""),
+ "Should have set the correct telemetry id for the normal engine"
+ );
+
+ await TestUtils.waitForCondition(
+ () =>
+ Glean.searchEnginePrivate.engineId.testGetValue() ==
+ (expected.private?.engineId ?? ""),
+ "Should have set the correct telemetry id for the private engine"
+ );
+
+ for (let property of [
+ "displayName",
+ "loadPath",
+ "submissionUrl",
+ "verified",
+ ]) {
+ if (property in expected.normal) {
+ Assert.equal(
+ Glean.searchEngineDefault[property].testGetValue(),
+ expected.normal[property] ?? "",
+ `Should have set ${property} correctly`
+ );
+ }
+ if (expected.private && property in expected.private) {
+ Assert.equal(
+ Glean.searchEnginePrivate[property].testGetValue(),
+ expected.private[property] ?? "",
+ `Should have set ${property} correctly`
+ );
+ }
+ }
+}
+
+/**
+ * A simple observer to ensure we get only the expected notifications.
+ */
+class SearchObserver {
+ constructor(expectedNotifications, returnEngineForNotification = false) {
+ this.observer = this.observer.bind(this);
+ this.deferred = PromiseUtils.defer();
+ this.expectedNotifications = expectedNotifications;
+ this.returnEngineForNotification = returnEngineForNotification;
+
+ Services.obs.addObserver(this.observer, SearchUtils.TOPIC_ENGINE_MODIFIED);
+
+ this.timeout = setTimeout(this.handleTimeout.bind(this), 1000);
+ }
+
+ get promise() {
+ return this.deferred.promise;
+ }
+
+ handleTimeout() {
+ this.deferred.reject(
+ new Error(
+ "Waiting for Notifications timed out, only received: " +
+ this.expectedNotifications.join(",")
+ )
+ );
+ }
+
+ observer(subject, topic, data) {
+ Assert.greater(
+ this.expectedNotifications.length,
+ 0,
+ "Should be expecting a notification"
+ );
+ Assert.equal(
+ data,
+ this.expectedNotifications[0],
+ "Should have received the next expected notification"
+ );
+
+ if (
+ this.returnEngineForNotification &&
+ data == this.returnEngineForNotification
+ ) {
+ this.engineToReturn = subject.QueryInterface(Ci.nsISearchEngine);
+ }
+
+ this.expectedNotifications.shift();
+
+ if (!this.expectedNotifications.length) {
+ clearTimeout(this.timeout);
+ delete this.timeout;
+ this.deferred.resolve(this.engineToReturn);
+ Services.obs.removeObserver(
+ this.observer,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ }
+ }
+}
+
+/**
+ * Some tests might trigger initialisation which will trigger the search settings
+ * update. We need to make sure we wait for that to finish before we exit, otherwise
+ * it may cause shutdown issues.
+ */
+let updatePromise = SearchTestUtils.promiseSearchNotification(
+ "settings-update-complete"
+);
+
+registerCleanupFunction(async () => {
+ if (Services.search.isInitialized) {
+ await updatePromise;
+ }
+});
+
+let consoleAllowList = [
+ // Harness issues.
+ 'property "localProfileDir" is non-configurable and can\'t be deleted',
+ 'property "profileDir" is non-configurable and can\'t be deleted',
+ // These can be emitted by `resource://services-settings/Utils.jsm` when
+ // remote settings is fetched (e.g. via IgnoreLists).
+ "NetworkError: Network request failed",
+ // Also remote settings, see bug 1812040.
+ "Unexpected content-type",
+];
+
+let endConsoleListening = TestUtils.listenForConsoleMessages();
+
+registerCleanupFunction(async () => {
+ let msgs = await endConsoleListening();
+ for (let msg of msgs) {
+ msg = msg.wrappedJSObject;
+ if (msg.level != "error") {
+ continue;
+ }
+
+ if (!msg.arguments?.length) {
+ Assert.ok(
+ false,
+ "Unexpected console message received during test: " + msg
+ );
+ } else {
+ let firstArg = msg.arguments[0];
+ // Use the appropriate message depending on the object supplied to
+ // the first argument.
+ let message = firstArg.messageContents ?? firstArg.message ?? firstArg;
+ if (!consoleAllowList.some(e => message.includes(e))) {
+ Assert.ok(
+ false,
+ "Unexpected console message received during test: " + message
+ );
+ }
+ }
+ }
+});
diff --git a/toolkit/components/search/tests/xpcshell/method-extensions/engines.json b/toolkit/components/search/tests/xpcshell/method-extensions/engines.json
new file mode 100644
index 0000000000..4c0a009f37
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/method-extensions/engines.json
@@ -0,0 +1,45 @@
+{
+ "data": [
+ {
+ "webExtension": {
+ "id": "get@search.mozilla.org"
+ },
+ "params": {
+ "searchUrlGetParams": [
+ { "name": "config", "value": "1" },
+ { "name": "search", "value": "{searchTerms}" }
+ ],
+ "suggestUrlGetParams": [
+ { "name": "config", "value": "1" },
+ { "name": "suggest", "value": "{searchTerms}" }
+ ]
+ },
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "default": "yes"
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "post@search.mozilla.org"
+ },
+ "params": {
+ "searchUrlPostParams": [
+ { "name": "config", "value": "1" },
+ { "name": "search", "value": "{searchTerms}" }
+ ],
+ "suggestUrlPostParams": [
+ { "name": "config", "value": "1" },
+ { "name": "suggest", "value": "{searchTerms}" }
+ ]
+ },
+ "appliesTo": [
+ {
+ "included": { "everywhere": true }
+ }
+ ]
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/method-extensions/get/manifest.json b/toolkit/components/search/tests/xpcshell/method-extensions/get/manifest.json
new file mode 100644
index 0000000000..a85cdaaa0f
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/method-extensions/get/manifest.json
@@ -0,0 +1,21 @@
+{
+ "name": "Get Engine",
+ "manifest_version": 2,
+ "version": "1.0",
+ "description": "Get engine to test get params",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "get@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "Get Engine",
+ "search_url": "https://example.com",
+ "search_url_get_params": "webExtension=1&search={searchTerms}",
+ "suggest_url": "https://example.com",
+ "suggest_url_get_params": "webExtension=1&suggest={searchTerms}"
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/method-extensions/post/manifest.json b/toolkit/components/search/tests/xpcshell/method-extensions/post/manifest.json
new file mode 100644
index 0000000000..dce9bfb512
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/method-extensions/post/manifest.json
@@ -0,0 +1,21 @@
+{
+ "name": "Post Engine",
+ "manifest_version": 2,
+ "version": "1.0",
+ "description": "Get engine to test ost params",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "post@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "Post Engine",
+ "search_url": "https://example.com",
+ "search_url_post_params": "webExtension=1&search={searchTerms}",
+ "suggest_url": "https://example.com",
+ "suggest_url_post_params": "webExtension=1&suggest={searchTerms}"
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/chromeicon.xml b/toolkit/components/search/tests/xpcshell/opensearch/chromeicon.xml
new file mode 100644
index 0000000000..856732c6d6
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/chromeicon.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-chromeicon</ShortName>
+<Image width="16" height="16">chrome://branding/content/icon16.png</Image>
+<Image width="32" height="32">chrome://branding/content/icon32.png</Image>
+<Url type="text/html" method="GET" template="http://www.google.com/search">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-insecurely-updated1.xml b/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-insecurely-updated1.xml
new file mode 100644
index 0000000000..3131c25f37
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-insecurely-updated1.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/">
+<ShortName>ii1</ShortName>
+<Description>Insecure and insecurely updated 1</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="http://example.com/ii1">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<Url type="application/opensearchdescription+xml"
+ rel="self"
+ template="http://example.com/ii1.xml" />
+<SearchForm>http://example.com/ii1</SearchForm>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-insecurely-updated2.xml b/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-insecurely-updated2.xml
new file mode 100644
index 0000000000..a3c850d4d9
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-insecurely-updated2.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/">
+<ShortName>ii2</ShortName>
+<Description>Insecure and insecurely updated 2</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="http://example.com/ii2">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<Url type="application/opensearchdescription+xml"
+ rel="self"
+ template="http://example.com/ii2.xml" />
+<SearchForm>http://example.com/ii2</SearchForm>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-no-update-url1.xml b/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-no-update-url1.xml
new file mode 100644
index 0000000000..75a5da8e7f
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-no-update-url1.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/">
+<ShortName>inu1</ShortName>
+<Description>Insecure and no update URL 1</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="http://example.com/inu1">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<SearchForm>http://example.com/inu1</SearchForm>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-securely-updated1.xml b/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-securely-updated1.xml
new file mode 100644
index 0000000000..9427747722
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-securely-updated1.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/">
+<ShortName>is1</ShortName>
+<Description>Insecure and securely updated 1</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="http://example.com/is1">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<Url type="application/opensearchdescription+xml"
+ rel="self"
+ template="https://example.com/is1.xml" />
+<SearchForm>http://example.com/is1</SearchForm>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/invalid.xml b/toolkit/components/search/tests/xpcshell/opensearch/invalid.xml
new file mode 100644
index 0000000000..e8efce6726
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/invalid.xml
@@ -0,0 +1 @@
+# An invalid xml engine file.
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/mozilla-ns.xml b/toolkit/components/search/tests/xpcshell/opensearch/mozilla-ns.xml
new file mode 100644
index 0000000000..f185f94868
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/mozilla-ns.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>mozilla-ns</ShortName>
+<Description>An engine using mozilla namespace</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="https://example.com/search">
+ <Param name="q" value="{searchTerms}"/>
+ <MozParam name="channel" condition="purpose" purpose="searchbar" value="test"/>
+</Url>
+<SearchForm>https://example.com/</SearchForm>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/post.xml b/toolkit/components/search/tests/xpcshell/opensearch/post.xml
new file mode 100644
index 0000000000..621e49c872
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/post.xml
@@ -0,0 +1,8 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
+ <ShortName>Post</ShortName>
+ <Url type="text/html" method="POST" template="https://example.com/post">
+ <Param name="searchterms" value="{searchTerms}"/>
+ </Url>
+ <Url type="text/html" method="POST" template="http://engine-rel-searchform-post.xml/POST" rel="searchform"/>
+ <SearchForm>http://engine-rel-searchform-post.xml/?search</SearchForm>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/resourceicon.xml b/toolkit/components/search/tests/xpcshell/opensearch/resourceicon.xml
new file mode 100644
index 0000000000..32861c34ea
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/resourceicon.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-resourceicon</ShortName>
+<Image width="16" height="16">resource://search-extensions/icon16.png</Image>
+<Image width="32" height="32">resource://search-extensions/icon32.png</Image>
+<Url type="text/html" method="GET" template="http://www.google.com/search">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-and-insecurely-updated1.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-insecurely-updated1.xml
new file mode 100644
index 0000000000..d8a62d0e18
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-insecurely-updated1.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/">
+<ShortName>si1</ShortName>
+<Description>Secure and insecurely updated 1</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="https://example.com/si1">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<Url type="application/opensearchdescription+xml"
+ rel="self"
+ template="http://example.com/si1.xml" />
+<SearchForm>https://example.com/si1</SearchForm>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-and-insecurely-updated2.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-insecurely-updated2.xml
new file mode 100644
index 0000000000..f707e5eb3d
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-insecurely-updated2.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/">
+<ShortName>si2</ShortName>
+<Description>Secure and insecurely updated 2</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="https://example.com/si2">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<Url type="application/opensearchdescription+xml"
+ rel="self"
+ template="http://example.com/si2.xml" />
+<SearchForm>https://example.com/si2</SearchForm>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-and-no-update-url1.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-no-update-url1.xml
new file mode 100644
index 0000000000..6dcbbb126c
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-no-update-url1.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/">
+<ShortName>snu1</ShortName>
+<Description>Secure and no update URL 1</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="https://example.com/snu1">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://example.com/snu1</SearchForm>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated-insecure-form.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated-insecure-form.xml
new file mode 100644
index 0000000000..15a0b6a517
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated-insecure-form.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/">
+<ShortName>ssif</ShortName>
+<Description>Secure and securely updated insecure form</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="https://example.com/ssif">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<Url type="application/opensearchdescription+xml"
+ rel="self"
+ template="https://example.com/ssif.xml" />
+<SearchForm>http://example.com/ssif</SearchForm>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated1.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated1.xml
new file mode 100644
index 0000000000..593c8bec8c
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated1.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/">
+<ShortName>ss1</ShortName>
+<Description>Secure and securely updated 1</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="https://example.com/ss1">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<Url type="application/opensearchdescription+xml"
+ rel="self"
+ template="https://example.com/ss1.xml" />
+<SearchForm>https://example.com/ss1</SearchForm>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated2.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated2.xml
new file mode 100644
index 0000000000..30a20b754a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated2.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/">
+<ShortName>ss2</ShortName>
+<Description>Secure and securely updated 2</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="https://example.com/ss2">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<Url type="application/opensearchdescription+xml"
+ rel="self"
+ template="https://example.com/ss2.xml" />
+<SearchForm>https://example.com/ss2</SearchForm>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated3.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated3.xml
new file mode 100644
index 0000000000..8b86a82199
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated3.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/">
+<ShortName>ss3</ShortName>
+<Description>Secure and securely updated 3</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="https://example.com/ss3">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<Url type="application/opensearchdescription+xml"
+ rel="self"
+ template="https://example.com/ss3.xml" />
+<SearchForm>https://example.com/ss3</SearchForm>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-localhost.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-localhost.xml
new file mode 100644
index 0000000000..89d96f2c43
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-localhost.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/">
+<ShortName>sl</ShortName>
+<Description>Secure localhost</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="http://localhost:8080">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<SearchForm>http://localhost:8080/sl</SearchForm>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-onionv2.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-onionv2.xml
new file mode 100644
index 0000000000..8da3995a71
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-onionv2.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/">
+<ShortName>sov2</ShortName>
+<Description>Secure onion v2</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="http://s3zkf3ortukqklec.onion">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<SearchForm>http://s3zkf3ortukqklec.onion/sov2</SearchForm>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-onionv3.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-onionv3.xml
new file mode 100644
index 0000000000..c8256ca28a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-onionv3.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/">
+<ShortName>sov3</ShortName>
+<Description>Secure onion v3</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="http://ydemw5wg5cseltau22u4fjfrmfshopaldpoznsirb3rgo2gv6uh4s2y5.onion">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<SearchForm>http://ydemw5wg5cseltau22u4fjfrmfshopaldpoznsirb3rgo2gv6uh4s2y5.onion/sov3</SearchForm>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/simple.xml b/toolkit/components/search/tests/xpcshell/opensearch/simple.xml
new file mode 100644
index 0000000000..ee38e51bca
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/simple.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/">
+<ShortName>simple</ShortName>
+<Description>A small test engine</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="https://example.com/search">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<SearchForm>https://example.com/</SearchForm>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/suggestion-alternate.xml b/toolkit/components/search/tests/xpcshell/opensearch/suggestion-alternate.xml
new file mode 100644
index 0000000000..7a961520b9
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/suggestion-alternate.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearchdescription/1.1/">
+<ShortName>suggestion-alternate</ShortName>
+<Description>A small engine with suggestions</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="https://example.com/search">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<Url type="application/json" rel="suggestions" method="GET"
+ template="https://example.com/suggest">
+ <Param name="suggestion" value="{searchTerms}"/>
+</Url>
+
+<SearchForm>https://example.com/</SearchForm>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/opensearch/suggestion.xml b/toolkit/components/search/tests/xpcshell/opensearch/suggestion.xml
new file mode 100644
index 0000000000..8d2f701a36
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/opensearch/suggestion.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearchdescription/1.0/">
+<ShortName>suggestion</ShortName>
+<Description>A small engine with suggestions</Description>
+<InputEncoding>windows-1252</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image>
+<Url type="text/html" method="GET" template="https://example.com/search">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<Url type="application/x-suggestions+json" method="GET"
+ template="https://example.com/suggest">
+ <Param name="suggestion" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="http://engine-rel-searchform.xml/?search" rel="searchform"/>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/head_searchconfig.js b/toolkit/components/search/tests/xpcshell/searchconfigs/head_searchconfig.js
new file mode 100644
index 0000000000..87ade57a51
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/head_searchconfig.js
@@ -0,0 +1,614 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
+ Region: "resource://gre/modules/Region.sys.mjs",
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+ SearchEngine: "resource://gre/modules/SearchEngine.sys.mjs",
+ SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs",
+ SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs",
+ SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
+
+const GLOBAL_SCOPE = this;
+const TEST_DEBUG = Services.env.get("TEST_DEBUG");
+
+const URLTYPE_SUGGEST_JSON = "application/x-suggestions+json";
+const URLTYPE_SEARCH_HTML = "text/html";
+const SUBMISSION_PURPOSES = [
+ "searchbar",
+ "keyword",
+ "contextmenu",
+ "homepage",
+ "newtab",
+];
+
+let engineSelector;
+
+/**
+ * This function is used to override the remote settings configuration
+ * if the SEARCH_CONFIG environment variable is set. This allows testing
+ * against a remote server.
+ */
+async function maybeSetupConfig() {
+ const SEARCH_CONFIG = Services.env.get("SEARCH_CONFIG");
+ if (SEARCH_CONFIG) {
+ if (!(SEARCH_CONFIG in SearchUtils.ENGINES_URLS)) {
+ throw new Error(`Invalid value for SEARCH_CONFIG`);
+ }
+ const url = SearchUtils.ENGINES_URLS[SEARCH_CONFIG];
+ const response = await fetch(url);
+ const config = await response.json();
+ const settings = await RemoteSettings(SearchUtils.SETTINGS_KEY);
+ sinon.stub(settings, "get").returns(config.data);
+ }
+}
+
+/**
+ * This class implements the test harness for search configuration tests.
+ * These tests are designed to ensure that the correct search engines are
+ * loaded for the various region/locale configurations.
+ *
+ * The configuration for each test is represented by an object having the
+ * following properties:
+ *
+ * - identifier (string)
+ * The identifier for the search engine under test.
+ * - default (object)
+ * An inclusion/exclusion configuration (see below) to detail when this engine
+ * should be listed as default.
+ *
+ * The inclusion/exclusion configuration is represented as an object having the
+ * following properties:
+ *
+ * - included (array)
+ * An optional array of region/locale pairs.
+ * - excluded (array)
+ * An optional array of region/locale pairs.
+ *
+ * If the object is empty, the engine is assumed not to be part of any locale/region
+ * pair.
+ * If the object has `excluded` but not `included`, then the engine is assumed to
+ * be part of every locale/region pair except for where it matches the exclusions.
+ *
+ * The region/locale pairs are represented as an object having the following
+ * properties:
+ *
+ * - region (array)
+ * An array of two-letter region codes.
+ * - locale (object)
+ * A locale object which may consist of:
+ * - matches (array)
+ * An array of locale strings which should exactly match the locale.
+ * - startsWith (array)
+ * An array of locale strings which the locale should start with.
+ */
+class SearchConfigTest {
+ /**
+ * @param {object} config
+ * The initial configuration for this test, see above.
+ */
+ constructor(config = {}) {
+ this._config = config;
+ }
+
+ /**
+ * Sets up the test.
+ *
+ * @param {string} [version]
+ * The version to simulate for running the tests.
+ */
+ async setup(version = "42.0") {
+ AddonTestUtils.init(GLOBAL_SCOPE);
+ AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ version,
+ version
+ );
+
+ await maybeSetupConfig();
+
+ // Disable region checks.
+ Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
+
+ // Enable separatePrivateDefault testing. We test with this on, as we have
+ // separate tests for ensuring the normal = private when this is off.
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ true
+ );
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+
+ // We must use the engine selector that the search service has created (if
+ // it has), as remote settings can only easily deal with us loading the
+ // configuration once - after that, it tries to access the network.
+ engineSelector =
+ Services.search.wrappedJSObject._engineSelector ||
+ new SearchEngineSelector();
+
+ // Note: we don't use the helper function here, so that we have at least
+ // one message output per process.
+ Assert.ok(
+ Services.search.isInitialized,
+ "Should have correctly initialized the search service"
+ );
+ }
+
+ /**
+ * Runs the test.
+ */
+ async run() {
+ const locales = await this._getLocales();
+ const regions = this._regions;
+
+ // We loop on region and then locale, so that we always cause a re-init
+ // when updating the requested/available locales.
+ for (let region of regions) {
+ for (let locale of locales) {
+ const engines = await this._getEngines(region, locale);
+ this._assertEngineRules([engines[0]], region, locale, "default");
+ const isPresent = this._assertAvailableEngines(region, locale, engines);
+ if (isPresent) {
+ this._assertEngineDetails(region, locale, engines);
+ }
+ }
+ }
+ }
+
+ async _getEngines(region, locale) {
+ let engines = [];
+ let configs = await engineSelector.fetchEngineConfiguration({
+ locale,
+ region: region || "default",
+ channel: SearchUtils.MODIFIED_APP_CHANNEL,
+ });
+ for (let config of configs.engines) {
+ let engine = await Services.search.wrappedJSObject._makeEngineFromConfig(
+ config
+ );
+ engines.push(engine);
+ }
+ return engines;
+ }
+
+ /**
+ * @returns {Set} the list of regions for the tests to run with.
+ */
+ get _regions() {
+ // TODO: The legacy configuration worked with null as an unknown region,
+ // for the search engine selector, we expect "default" but apply the
+ // fallback in _getEngines. Once we remove the legacy configuration, we can
+ // simplify this.
+ if (TEST_DEBUG) {
+ return new Set(["by", "cn", "kz", "us", "ru", "tr", null]);
+ }
+ return [...Services.intl.getAvailableLocaleDisplayNames("region"), null];
+ }
+
+ /**
+ * @returns {Array} the list of locales for the tests to run with.
+ */
+ async _getLocales() {
+ if (TEST_DEBUG) {
+ return ["be", "en-US", "kk", "tr", "ru", "zh-CN", "ach", "unknown"];
+ }
+ const data = await IOUtils.readUTF8(do_get_file("all-locales").path);
+ // "en-US" is not in all-locales as it is the default locale
+ // add it manually to ensure it is tested.
+ let locales = [...data.split("\n").filter(e => e != ""), "en-US"];
+ // BCP47 requires all variants are 5-8 characters long. Our
+ // build sytem uses the short `mac` variant, this is invalid, and inside
+ // the app we turn it into `ja-JP-macos`
+ locales = locales.map(l => (l == "ja-JP-mac" ? "ja-JP-macos" : l));
+ // The locale sometimes can be unknown or a strange name, e.g. if the updater
+ // is disabled, it may be "und", add one here so we know what happens if we
+ // hit it.
+ locales.push("unknown");
+ return locales;
+ }
+
+ /**
+ * Determines if a locale matches with a locales section in the configuration.
+ *
+ * @param {object} locales
+ * The config locales config, containing the locals to match against.
+ * @param {Array} [locales.matches]
+ * Array of locale names to match exactly.
+ * @param {Array} [locales.startsWith]
+ * Array of locale names to match the start.
+ * @param {string} locale
+ * The two-letter locale code.
+ * @returns {boolean}
+ * True if the locale matches.
+ */
+ _localeIncludes(locales, locale) {
+ if ("matches" in locales && locales.matches.includes(locale)) {
+ return true;
+ }
+ if ("startsWith" in locales) {
+ return !!locales.startsWith.find(element => locale.startsWith(element));
+ }
+
+ return false;
+ }
+
+ /**
+ * Determines if a locale/region pair match a section of the configuration.
+ *
+ * @param {object} section
+ * The configuration section to match against.
+ * @param {string} region
+ * The two-letter region code.
+ * @param {string} locale
+ * The two-letter locale code.
+ * @returns {boolean}
+ * True if the locale/region pair matches the section.
+ */
+ _localeRegionInSection(section, region, locale) {
+ for (const { regions, locales } of section) {
+ // If we only specify a regions or locales section then
+ // it is always considered included in the other section.
+ const inRegions = !regions || regions.includes(region);
+ const inLocales = !locales || this._localeIncludes(locales, locale);
+ if (inRegions && inLocales) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Helper function to find an engine from within a list.
+ *
+ * @param {Array} engines
+ * The list of engines to check.
+ * @param {string} identifier
+ * The identifier to look for in the list.
+ * @param {boolean} exactMatch
+ * Whether to use an exactMatch for the identifier.
+ * @returns {Engine}
+ * Returns the engine if found, null otherwise.
+ */
+ _findEngine(engines, identifier, exactMatch) {
+ return engines.find(engine =>
+ exactMatch
+ ? engine.identifier == identifier
+ : engine.identifier.startsWith(identifier)
+ );
+ }
+
+ /**
+ * Asserts whether the engines rules defined in the configuration are met.
+ *
+ * @param {Array} engines
+ * The list of engines to check.
+ * @param {string} region
+ * The two-letter region code.
+ * @param {string} locale
+ * The two-letter locale code.
+ * @param {string} section
+ * The section of the configuration to check.
+ * @returns {boolean}
+ * Returns true if the engine is expected to be present, false otherwise.
+ */
+ _assertEngineRules(engines, region, locale, section) {
+ const infoString = `region: "${region}" locale: "${locale}"`;
+ const config = this._config[section];
+ const hasIncluded = "included" in config;
+ const hasExcluded = "excluded" in config;
+ const identifierIncluded = !!this._findEngine(
+ engines,
+ this._config.identifier,
+ this._config.identifierExactMatch ?? false
+ );
+
+ // If there's not included/excluded, then this shouldn't be the default anywhere.
+ if (section == "default" && !hasIncluded && !hasExcluded) {
+ this.assertOk(
+ !identifierIncluded,
+ `Should not be ${section} for any locale/region,
+ currently set for ${infoString}`
+ );
+ return false;
+ }
+
+ // If there's no included section, we assume the engine is default everywhere
+ // and we should apply the exclusions instead.
+ let included =
+ hasIncluded &&
+ this._localeRegionInSection(config.included, region, locale);
+
+ let excluded =
+ hasExcluded &&
+ this._localeRegionInSection(config.excluded, region, locale);
+ if (
+ (included && (!hasExcluded || !excluded)) ||
+ (!hasIncluded && hasExcluded && !excluded)
+ ) {
+ this.assertOk(
+ identifierIncluded,
+ `Should be ${section} for ${infoString}`
+ );
+ return true;
+ }
+ this.assertOk(
+ !identifierIncluded,
+ `Should not be ${section} for ${infoString}`
+ );
+ return false;
+ }
+
+ /**
+ * Asserts whether the engine is correctly set as default or not.
+ *
+ * @param {string} region
+ * The two-letter region code.
+ * @param {string} locale
+ * The two-letter locale code.
+ */
+ _assertDefaultEngines(region, locale) {
+ this._assertEngineRules(
+ [Services.search.appDefaultEngine],
+ region,
+ locale,
+ "default"
+ );
+ // At the moment, this uses the same section as the normal default, as
+ // we don't set this differently for any region/locale.
+ this._assertEngineRules(
+ [Services.search.appPrivateDefaultEngine],
+ region,
+ locale,
+ "default"
+ );
+ }
+
+ /**
+ * Asserts whether the engine is correctly available or not.
+ *
+ * @param {string} region
+ * The two-letter region code.
+ * @param {string} locale
+ * The two-letter locale code.
+ * @param {Array} engines
+ * The current visible engines.
+ * @returns {boolean}
+ * Returns true if the engine is expected to be present, false otherwise.
+ */
+ _assertAvailableEngines(region, locale, engines) {
+ return this._assertEngineRules(engines, region, locale, "available");
+ }
+
+ /**
+ * Asserts the engine follows various rules.
+ *
+ * @param {string} region
+ * The two-letter region code.
+ * @param {string} locale
+ * The two-letter locale code.
+ * @param {Array} engines
+ * The current visible engines.
+ */
+ _assertEngineDetails(region, locale, engines) {
+ const details = this._config.details.filter(value => {
+ const included = this._localeRegionInSection(
+ value.included,
+ region,
+ locale
+ );
+ const excluded =
+ value.excluded &&
+ this._localeRegionInSection(value.excluded, region, locale);
+ return included && !excluded;
+ });
+ this.assertEqual(
+ details.length,
+ 1,
+ `Should have just one details section for region: ${region} locale: ${locale}`
+ );
+
+ const engine = this._findEngine(
+ engines,
+ this._config.identifier,
+ this._config.identifierExactMatch ?? false
+ );
+ this.assertOk(engine, "Should have an engine present");
+
+ if (this._config.aliases) {
+ this.assertDeepEqual(
+ engine.aliases,
+ this._config.aliases,
+ "Should have the correct aliases for the engine"
+ );
+ }
+
+ const location = `in region:${region}, locale:${locale}`;
+
+ for (const rule of details) {
+ this._assertCorrectDomains(location, engine, rule);
+ if (rule.codes) {
+ this._assertCorrectCodes(location, engine, rule);
+ }
+ if (rule.searchUrlCode || rule.suggestUrlCode) {
+ this._assertCorrectUrlCode(location, engine, rule);
+ }
+ if (rule.aliases) {
+ this.assertDeepEqual(
+ engine.aliases,
+ rule.aliases,
+ "Should have the correct aliases for the engine"
+ );
+ }
+ if (rule.telemetryId) {
+ this.assertEqual(
+ engine.telemetryId,
+ rule.telemetryId,
+ `Should have the correct telemetryId ${location}.`
+ );
+ }
+ }
+ }
+
+ /**
+ * Asserts whether the engine is using the correct domains or not.
+ *
+ * @param {string} location
+ * Debug string with locale + region information.
+ * @param {object} engine
+ * The engine being tested.
+ * @param {object} rules
+ * Rules to test.
+ */
+ _assertCorrectDomains(location, engine, rules) {
+ this.assertOk(
+ rules.domain,
+ `Should have an expectedDomain for the engine ${location}`
+ );
+
+ const searchForm = new URL(engine.searchForm);
+ this.assertOk(
+ searchForm.host.endsWith(rules.domain),
+ `Should have the correct search form domain ${location}.
+ Got "${searchForm.host}", expected to end with "${rules.domain}".`
+ );
+
+ let submission = engine.getSubmission("test", URLTYPE_SEARCH_HTML);
+
+ this.assertOk(
+ submission.uri.host.endsWith(rules.domain),
+ `Should have the correct domain for type: ${URLTYPE_SEARCH_HTML} ${location}.
+ Got "${submission.uri.host}", expected to end with "${rules.domain}".`
+ );
+
+ submission = engine.getSubmission("test", URLTYPE_SUGGEST_JSON);
+ if (this._config.noSuggestionsURL || rules.noSuggestionsURL) {
+ this.assertOk(!submission, "Should not have a submission url");
+ } else if (this._config.suggestionUrlBase) {
+ this.assertEqual(
+ submission.uri.prePath + submission.uri.filePath,
+ this._config.suggestionUrlBase,
+ `Should have the correct domain for type: ${URLTYPE_SUGGEST_JSON} ${location}.`
+ );
+ this.assertOk(
+ submission.uri.query.includes(rules.suggestUrlCode),
+ `Should have the code in the uri`
+ );
+ }
+ }
+
+ /**
+ * Asserts whether the engine is using the correct codes or not.
+ *
+ * @param {string} location
+ * Debug string with locale + region information.
+ * @param {object} engine
+ * The engine being tested.
+ * @param {object} rules
+ * Rules to test.
+ */
+ _assertCorrectCodes(location, engine, rules) {
+ for (const purpose of SUBMISSION_PURPOSES) {
+ // Don't need to repeat the code if we use it for all purposes.
+ const code =
+ typeof rules.codes === "string" ? rules.codes : rules.codes[purpose];
+ const submission = engine.getSubmission("test", "text/html", purpose);
+ const submissionQueryParams = submission.uri.query.split("&");
+ this.assertOk(
+ submissionQueryParams.includes(code),
+ `Expected "${code}" in url "${submission.uri.spec}" from purpose "${purpose}" ${location}`
+ );
+
+ const paramName = code.split("=")[0];
+ this.assertOk(
+ submissionQueryParams.filter(param => param.startsWith(paramName))
+ .length == 1,
+ `Expected only one "${paramName}" parameter in "${submission.uri.spec}" from purpose "${purpose}" ${location}`
+ );
+ }
+ }
+
+ /**
+ * Asserts whether the engine is using the correct URL codes or not.
+ *
+ * @param {string} location
+ * Debug string with locale + region information.
+ * @param {object} engine
+ * The engine being tested.
+ * @param {object} rule
+ * Rules to test.
+ */
+ _assertCorrectUrlCode(location, engine, rule) {
+ if (rule.searchUrlCode) {
+ const submission = engine.getSubmission("test", URLTYPE_SEARCH_HTML);
+ this.assertOk(
+ submission.uri.query.split("&").includes(rule.searchUrlCode),
+ `Expected "${rule.searchUrlCode}" in search url "${submission.uri.spec}"`
+ );
+ let uri = engine.searchForm;
+ this.assertOk(
+ !uri.includes(rule.searchUrlCode),
+ `"${rule.searchUrlCode}" should not be in the search form URL.`
+ );
+ }
+ if (rule.searchUrlCodeNotInQuery) {
+ const submission = engine.getSubmission("test", URLTYPE_SEARCH_HTML);
+ this.assertOk(
+ submission.uri.includes(rule.searchUrlCodeNotInQuery),
+ `Expected "${rule.searchUrlCodeNotInQuery}" in search url "${submission.uri.spec}"`
+ );
+ }
+ if (rule.suggestUrlCode) {
+ const submission = engine.getSubmission("test", URLTYPE_SUGGEST_JSON);
+ this.assertOk(
+ submission.uri.query.split("&").includes(rule.suggestUrlCode),
+ `Expected "${rule.suggestUrlCode}" in suggestion url "${submission.uri.spec}"`
+ );
+ }
+ }
+
+ /**
+ * Helper functions which avoid outputting test results when there are no
+ * failures. These help the tests to run faster, and avoid clogging up the
+ * python test runner process.
+ */
+
+ assertOk(value, message) {
+ if (!value || TEST_DEBUG) {
+ Assert.ok(value, message);
+ }
+ }
+
+ assertEqual(actual, expected, message) {
+ if (actual != expected || TEST_DEBUG) {
+ Assert.equal(actual, expected, message);
+ }
+ }
+
+ assertDeepEqual(actual, expected, message) {
+ if (!ObjectUtils.deepEqual(actual, expected)) {
+ Assert.deepEqual(actual, expected, message);
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_amazon.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_amazon.js
new file mode 100644
index 0000000000..30d4d478f0
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_amazon.js
@@ -0,0 +1,361 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const mainShippedRegions = [
+ "at",
+ "au",
+ "be",
+ "ca",
+ "ch",
+ "cn",
+ "de",
+ "es",
+ "fr",
+ "mc",
+ "gb",
+ "ie",
+ "it",
+ "jp",
+ "nl",
+ "pt",
+ "se",
+ "sm",
+ "us",
+ "va",
+];
+
+const amazondotcomLocales = [
+ "ach",
+ "af",
+ "ar",
+ "az",
+ "bg",
+ "cak",
+ "cy",
+ "da",
+ "el",
+ "en-US",
+ "en-GB",
+ "eo",
+ "es-AR",
+ "eu",
+ "fa",
+ "ga-IE",
+ "gd",
+ "gl",
+ "gn",
+ "hr",
+ "hy-AM",
+ "ia",
+ "is",
+ "ka",
+ "km",
+ "lt",
+ "mk",
+ "ms",
+ "my",
+ "nb-NO",
+ "nn-NO",
+ "pt-PT",
+ "ro",
+ "si",
+ "sq",
+ "sr",
+ "th",
+ "tl",
+ "trs",
+ "uz",
+];
+
+const test = new SearchConfigTest({
+ identifier: "amazon",
+ default: {
+ // Not default anywhere.
+ },
+ available: {
+ included: [
+ {
+ // The main regions we ship Amazon to. Below this are special cases.
+ regions: mainShippedRegions,
+ },
+ {
+ // Amazon.com ships to all of these locales, excluding the ones where
+ // we ship other items, but it does not matter that they are duplicated
+ // in the available list.
+ locales: {
+ matches: amazondotcomLocales,
+ },
+ },
+ {
+ // Amazon.in
+ regions: ["in"],
+ locales: {
+ matches: ["bn", "gu-IN", "kn", "mr", "pa-IN", "ta", "te", "ur"],
+ },
+ },
+ ],
+ excluded: [
+ {
+ // Extra special case for cn as that only ships to the one locale.
+ regions: ["in"],
+ locales: {
+ matches: amazondotcomLocales,
+ },
+ },
+ ],
+ },
+ details: [
+ {
+ domain: "amazon.com.au",
+ telemetryId: "amazon-au",
+ aliases: ["@amazon"],
+ included: [
+ {
+ regions: ["au"],
+ },
+ ],
+ suggestionUrlBase: "https://completion.amazon.com.au/search/complete",
+ suggestUrlCode: "mkt=111172",
+ },
+ {
+ domain: "amazon.ca",
+ telemetryId: "amazon-ca",
+ aliases: ["@amazon"],
+ included: [
+ {
+ regions: ["ca"],
+ },
+ ],
+ searchUrlCode: "tag=mozillacanada-20",
+ suggestionUrlBase: "https://completion.amazon.ca/search/complete",
+ suggestUrlCode: "mkt=7",
+ },
+ {
+ domain: "amazon.cn",
+ telemetryId: "amazondotcn",
+ included: [
+ {
+ regions: ["cn"],
+ },
+ ],
+ searchUrlCode: "ix=sunray",
+ noSuggestionsURL: true,
+ },
+ {
+ domain: "amazon.co.jp",
+ telemetryId: "amazon-jp",
+ aliases: ["@amazon"],
+ included: [
+ {
+ regions: ["jp"],
+ },
+ ],
+ searchUrlCode: "tag=mozillajapan-fx-22",
+ suggestionUrlBase: "https://completion.amazon.co.jp/search/complete",
+ suggestUrlCode: "mkt=6",
+ },
+ {
+ domain: "amazon.co.uk",
+ telemetryId: "amazon-en-GB",
+ aliases: ["@amazon"],
+ included: [
+ {
+ regions: ["gb", "ie"],
+ },
+ ],
+ searchUrlCode: "tag=firefox-uk-21",
+ suggestionUrlBase: "https://completion.amazon.co.uk/search/complete",
+ suggestUrlCode: "mkt=3",
+ },
+ {
+ domain: "amazon.com",
+ telemetryId: "amazondotcom-us",
+ aliases: ["@amazon"],
+ included: [
+ {
+ regions: ["us"],
+ },
+ ],
+ searchUrlCode: "tag=moz-us-20",
+ },
+ {
+ domain: "amazon.com",
+ telemetryId: "amazondotcom",
+ aliases: ["@amazon"],
+ included: [
+ {
+ locales: {
+ matches: amazondotcomLocales,
+ },
+ },
+ ],
+ excluded: [{ regions: mainShippedRegions }],
+ searchUrlCode: "tag=mozilla-20",
+ },
+ {
+ domain: "amazon.de",
+ telemetryId: "amazon-de",
+ aliases: ["@amazon"],
+ included: [
+ {
+ regions: ["at", "ch", "de"],
+ },
+ ],
+ searchUrlCode: "tag=firefox-de-21",
+ suggestionUrlBase: "https://completion.amazon.de/search/complete",
+ suggestUrlCode: "mkt=4",
+ },
+ {
+ domain: "amazon.es",
+ telemetryId: "amazon-es",
+ aliases: ["@amazon"],
+ included: [
+ {
+ regions: ["es", "pt"],
+ },
+ ],
+ searchUrlCode: "tag=mozillaspain-21",
+ suggestionUrlBase: "https://completion.amazon.es/search/complete",
+ suggestUrlCode: "mkt=44551",
+ },
+ {
+ domain: "amazon.fr",
+ telemetryId: "amazon-france",
+ aliases: ["@amazon"],
+ included: [
+ {
+ regions: ["fr", "mc"],
+ },
+ {
+ regions: ["be"],
+ locales: {
+ matches: ["fr"],
+ },
+ },
+ ],
+ searchUrlCode: "tag=firefox-fr-21",
+ suggestionUrlBase: "https://completion.amazon.fr/search/complete",
+ suggestUrlCode: "mkt=5",
+ },
+ {
+ domain: "amazon.in",
+ telemetryId: "amazon-in",
+ aliases: ["@amazon"],
+ included: [
+ {
+ locales: {
+ matches: ["bn", "gu-IN", "kn", "mr", "pa-IN", "ta", "te", "ur"],
+ },
+ regions: ["in"],
+ },
+ ],
+ suggestionUrlBase: "https://completion.amazon.in/search/complete",
+ suggestUrlCode: "mkt=44571",
+ },
+ {
+ domain: "amazon.it",
+ telemetryId: "amazon-it",
+ aliases: ["@amazon"],
+ included: [
+ {
+ regions: ["it", "sm", "va"],
+ },
+ ],
+ searchUrlCode: "tag=firefoxit-21",
+ suggestionUrlBase: "https://completion.amazon.it/search/complete",
+ suggestUrlCode: "mkt=35691",
+ },
+ {
+ domain: "amazon.nl",
+ telemetryId: "amazon-nl",
+ aliases: ["@amazon"],
+ included: [
+ {
+ regions: ["nl"],
+ },
+ ],
+ searchUrlCode: "tag=mozillanether-21",
+ suggestionUrlBase: "https://completion.amazon.nl/search/complete",
+ suggestUrlCode: "mkt=328451",
+ },
+ {
+ domain: "amazon.nl",
+ telemetryId: "amazon-nl",
+ aliases: ["@amazon"],
+ included: [
+ {
+ regions: ["be"],
+ },
+ ],
+ excluded: [
+ {
+ locales: {
+ matches: ["fr"],
+ },
+ },
+ ],
+ searchUrlCode: "tag=mozillanether-21",
+ suggestionUrlBase: "https://completion.amazon.nl/search/complete",
+ suggestUrlCode: "mkt=328451",
+ },
+ {
+ domain: "amazon.se",
+ telemetryId: "amazon-se",
+ aliases: ["@amazon"],
+ included: [
+ {
+ regions: ["se"],
+ },
+ ],
+ searchUrlCode: "tag=mozillasweede-21",
+ suggestionUrlBase: "https://completion.amazon.se/search/complete",
+ suggestUrlCode: "mkt=704403121",
+ },
+ ],
+});
+
+add_task(async function setup() {
+ // We only need to do setup on one of the tests.
+ await test.setup("89.0");
+});
+
+add_task(async function test_searchConfig_amazon() {
+ await test.run();
+});
+
+add_task(async function test_searchConfig_amazon_pre89() {
+ AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "88.0",
+ "88.0"
+ );
+ // For pre-89, Amazon has a slightly different config.
+ let details = test._config.details.find(
+ d => d.telemetryId == "amazondotcom-us"
+ );
+ details.telemetryId = "amazondotcom";
+ details.searchUrlCode = "tag=mozilla-20";
+
+ // nl not present due to urls that don't work.
+ let availableIn = test._config.available.included;
+ availableIn[0].regions = availableIn[0].regions.filter(
+ r => r != "be" && r != "nl"
+ );
+ availableIn.push({
+ regions: ["be"],
+ locales: {
+ matches: ["fr"],
+ },
+ });
+ // Due to the way the exclusions work, no Amazon present in nl/be in the
+ // dot com locales for pre-89.
+ test._config.available.excluded[0].regions.push("be", "nl");
+ test._config.details = test._config.details.filter(
+ d => d.telemetryId != "amazon-nl"
+ );
+
+ await test.run();
+});
diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_baidu.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_baidu.js
new file mode 100644
index 0000000000..01094b260a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_baidu.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const test = new SearchConfigTest({
+ identifier: "baidu",
+ aliases: ["@\u767E\u5EA6", "@baidu"],
+ default: {
+ included: [
+ {
+ regions: ["cn"],
+ locales: {
+ matches: ["zh-CN"],
+ },
+ },
+ ],
+ },
+ available: {
+ included: [
+ {
+ locales: {
+ matches: ["zh-CN"],
+ },
+ },
+ ],
+ },
+ details: [
+ {
+ included: [{}],
+ domain: "baidu.com",
+ telemetryId: "baidu",
+ },
+ ],
+});
+
+add_task(async function setup() {
+ await test.setup();
+});
+
+add_task(async function test_searchConfig_baidu() {
+ await test.run();
+});
diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_bing.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_bing.js
new file mode 100644
index 0000000000..5e91c9f49b
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_bing.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const test = new SearchConfigTest({
+ identifier: "bing",
+ aliases: ["@bing"],
+ default: {
+ // Not included anywhere.
+ },
+ available: {
+ included: [
+ {
+ // regions: [
+ // These arent currently enforced.
+ // "au", "at", "be", "br", "ca", "fi", "fr", "de",
+ // "in", "ie", "it", "jp", "my", "mx", "nl", "nz",
+ // "no", "sg", "es", "se", "ch", "gb", "us",
+ // ],
+ locales: {
+ matches: [
+ "ach",
+ "af",
+ "an",
+ "ar",
+ "ast",
+ "az",
+ "bs",
+ "ca",
+ "ca-valencia",
+ "cak",
+ "cs",
+ "cy",
+ "da",
+ "de",
+ "dsb",
+ "el",
+ "eo",
+ "es-CL",
+ "es-ES",
+ "es-MX",
+ "eu",
+ "fa",
+ "ff",
+ "fi",
+ "fr",
+ "fur",
+ "fy-NL",
+ "gd",
+ "gl",
+ "gn",
+ "gu-IN",
+ "he",
+ "hi-IN",
+ "hr",
+ "hsb",
+ "hy-AM",
+ "ia",
+ "id",
+ "is",
+ "it",
+ "ja-JP-macos",
+ "ja",
+ "ka",
+ "kab",
+ "km",
+ "kn",
+ "lij",
+ "lo",
+ "lt",
+ "meh",
+ "mk",
+ "ms",
+ "my",
+ "nb-NO",
+ "ne-NP",
+ "nl",
+ "nn-NO",
+ "oc",
+ "pa-IN",
+ "pt-BR",
+ "rm",
+ "ro",
+ "sc",
+ "sco",
+ "son",
+ "sq",
+ "sr",
+ "sv-SE",
+ "te",
+ "th",
+ "tl",
+ "tr",
+ "trs",
+ "uk",
+ "ur",
+ "uz",
+ "wo",
+ "xh",
+ "zh-CN",
+ ],
+ startsWith: ["bn", "en"],
+ },
+ },
+ ],
+ },
+ details: [
+ {
+ included: [{}],
+ domain: "bing.com",
+ telemetryId:
+ SearchUtils.MODIFIED_APP_CHANNEL == "esr" ? "bing-esr" : "bing",
+ codes: {
+ searchbar: "form=MOZSBR",
+ keyword: "form=MOZLBR",
+ contextmenu: "form=MOZCON",
+ homepage: "form=MOZSPG",
+ newtab: "form=MOZTSB",
+ },
+ searchUrlCode:
+ SearchUtils.MODIFIED_APP_CHANNEL == "esr" ? "pc=MOZR" : "pc=MOZI",
+ },
+ ],
+});
+
+add_task(async function setup() {
+ await test.setup();
+});
+
+add_task(async function test_searchConfig_bing() {
+ await test.run();
+});
diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_distributions.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_distributions.js
new file mode 100644
index 0000000000..0b44a5509e
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_distributions.js
@@ -0,0 +1,346 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs",
+ SearchService: "resource://gre/modules/SearchService.sys.mjs",
+});
+
+const tests = [];
+
+for (let canonicalId of ["canonical", "canonical-001"]) {
+ tests.push({
+ locale: "en-US",
+ region: "US",
+ distribution: canonicalId,
+ test: engines =>
+ hasParams(engines, "Google", "searchbar", "client=ubuntu") &&
+ hasParams(engines, "Google", "searchbar", "channel=fs") &&
+ hasTelemetryId(engines, "Google", "google-canonical"),
+ });
+
+ tests.push({
+ locale: "en-US",
+ region: "GB",
+ distribution: canonicalId,
+ test: engines =>
+ hasParams(engines, "Google", "searchbar", "client=ubuntu") &&
+ hasParams(engines, "Google", "searchbar", "channel=fs") &&
+ hasTelemetryId(engines, "Google", "google-canonical"),
+ });
+}
+
+tests.push({
+ locale: "en-US",
+ region: "US",
+ distribution: "canonical-002",
+ test: engines =>
+ hasParams(engines, "Google", "searchbar", "client=ubuntu-sn") &&
+ hasParams(engines, "Google", "searchbar", "channel=fs") &&
+ hasTelemetryId(engines, "Google", "google-ubuntu-sn"),
+});
+
+tests.push({
+ locale: "en-US",
+ region: "GB",
+ distribution: "canonical-002",
+ test: engines =>
+ hasParams(engines, "Google", "searchbar", "client=ubuntu-sn") &&
+ hasParams(engines, "Google", "searchbar", "channel=fs") &&
+ hasTelemetryId(engines, "Google", "google-ubuntu-sn"),
+});
+
+tests.push({
+ locale: "zh-CN",
+ region: "CN",
+ distribution: "MozillaOnline",
+ test: engines =>
+ hasParams(engines, "亚马逊", "searchbar", "ie=UTF8") &&
+ hasParams(engines, "亚马逊", "suggestions", "tag=mozilla") &&
+ hasParams(engines, "亚马逊", "homepage", "camp=536") &&
+ hasParams(engines, "亚马逊", "homepage", "creative=3200") &&
+ hasParams(engines, "亚马逊", "homepage", "index=aps") &&
+ hasParams(engines, "亚马逊", "homepage", "linkCode=ur2") &&
+ hasEnginesFirst(engines, ["百度", "Bing", "Google", "亚马逊", "维基百科"]),
+});
+
+tests.push({
+ locale: "fr",
+ distribution: "qwant-001",
+ test: engines =>
+ hasParams(engines, "Qwant", "searchbar", "client=firefoxqwant") &&
+ hasDefault(engines, "Qwant") &&
+ hasEnginesFirst(engines, ["Qwant", "Qwant Junior"]),
+});
+
+tests.push({
+ locale: "fr",
+ distribution: "qwant-001",
+ test: engines =>
+ hasParams(engines, "Qwant Junior", "searchbar", "client=firefoxqwant"),
+});
+
+tests.push({
+ locale: "fr",
+ distribution: "qwant-002",
+ test: engines =>
+ hasParams(engines, "Qwant", "searchbar", "client=firefoxqwant") &&
+ hasDefault(engines, "Qwant") &&
+ hasEnginesFirst(engines, ["Qwant", "Qwant Junior"]),
+});
+
+tests.push({
+ locale: "fr",
+ distribution: "qwant-002",
+ test: engines =>
+ hasParams(engines, "Qwant Junior", "searchbar", "client=firefoxqwant"),
+});
+
+for (const locale of ["en-US", "de"]) {
+ tests.push({
+ locale,
+ distribution: "1und1",
+ test: engines =>
+ hasParams(engines, "1&1 Suche", "searchbar", "enc=UTF-8") &&
+ hasDefault(engines, "1&1 Suche") &&
+ hasEnginesFirst(engines, ["1&1 Suche"]),
+ });
+
+ tests.push({
+ locale,
+ distribution: "gmx",
+ test: engines =>
+ hasParams(engines, "GMX Suche", "searchbar", "enc=UTF-8") &&
+ hasDefault(engines, "GMX Suche") &&
+ hasEnginesFirst(engines, ["GMX Suche"]),
+ });
+
+ tests.push({
+ locale,
+ distribution: "gmx",
+ test: engines =>
+ hasParams(engines, "GMX Shopping", "searchbar", "origin=br_osd"),
+ });
+
+ tests.push({
+ locale,
+ distribution: "mail.com",
+ test: engines =>
+ hasParams(engines, "mail.com search", "searchbar", "enc=UTF-8") &&
+ hasDefault(engines, "mail.com search") &&
+ hasEnginesFirst(engines, ["mail.com search"]),
+ });
+
+ tests.push({
+ locale,
+ distribution: "webde",
+ test: engines =>
+ hasParams(engines, "WEB.DE Suche", "searchbar", "enc=UTF-8") &&
+ hasDefault(engines, "WEB.DE Suche") &&
+ hasEnginesFirst(engines, ["WEB.DE Suche"]),
+ });
+}
+
+tests.push({
+ locale: "ru",
+ region: "RU",
+ distribution: "gmx",
+ test: engines => hasDefault(engines, "GMX Suche"),
+});
+
+tests.push({
+ locale: "en-GB",
+ distribution: "gmxcouk",
+ test: engines =>
+ hasURLs(
+ engines,
+ "GMX Search",
+ "https://go.gmx.co.uk/br/moz_search_web/?enc=UTF-8&q=test",
+ "https://suggestplugin.gmx.co.uk/s?q=test&brand=gmxcouk&origin=moz_splugin_ff&enc=UTF-8"
+ ) &&
+ hasDefault(engines, "GMX Search") &&
+ hasEnginesFirst(engines, ["GMX Search"]),
+});
+
+tests.push({
+ locale: "ru",
+ region: "RU",
+ distribution: "gmxcouk",
+ test: engines => hasDefault(engines, "GMX Search"),
+});
+
+tests.push({
+ locale: "es",
+ distribution: "gmxes",
+ test: engines =>
+ hasURLs(
+ engines,
+ "GMX - Búsqueda web",
+ "https://go.gmx.es/br/moz_search_web/?enc=UTF-8&q=test",
+ "https://suggestplugin.gmx.es/s?q=test&brand=gmxes&origin=moz_splugin_ff&enc=UTF-8"
+ ) &&
+ hasDefault(engines, "GMX Search") &&
+ hasEnginesFirst(engines, ["GMX Search"]),
+});
+
+tests.push({
+ locale: "ru",
+ region: "RU",
+ distribution: "gmxes",
+ test: engines => hasDefault(engines, "GMX - Búsqueda web"),
+});
+
+tests.push({
+ locale: "fr",
+ distribution: "gmxfr",
+ test: engines =>
+ hasURLs(
+ engines,
+ "GMX - Recherche web",
+ "https://go.gmx.fr/br/moz_search_web/?enc=UTF-8&q=test",
+ "https://suggestplugin.gmx.fr/s?q=test&brand=gmxfr&origin=moz_splugin_ff&enc=UTF-8"
+ ) &&
+ hasDefault(engines, "GMX Search") &&
+ hasEnginesFirst(engines, ["GMX Search"]),
+});
+
+tests.push({
+ locale: "ru",
+ region: "RU",
+ distribution: "gmxfr",
+ test: engines => hasDefault(engines, "GMX - Recherche web"),
+});
+
+tests.push({
+ locale: "en-US",
+ region: "US",
+ distribution: "mint-001",
+ test: engines =>
+ hasParams(engines, "DuckDuckGo", "searchbar", "t=lm") &&
+ hasParams(engines, "Google", "searchbar", "client=firefox-b-1-lm") &&
+ hasDefault(engines, "Google") &&
+ hasEnginesFirst(engines, ["Google"]) &&
+ hasTelemetryId(engines, "Google", "google-b-1-lm"),
+});
+
+tests.push({
+ locale: "en-GB",
+ region: "GB",
+ distribution: "mint-001",
+ test: engines =>
+ hasParams(engines, "DuckDuckGo", "searchbar", "t=lm") &&
+ hasParams(engines, "Google", "searchbar", "client=firefox-b-lm") &&
+ hasDefault(engines, "Google") &&
+ hasEnginesFirst(engines, ["Google"]) &&
+ hasTelemetryId(engines, "Google", "google-b-lm"),
+});
+
+function hasURLs(engines, engineName, url, suggestURL) {
+ let engine = engines.find(e => e._name === engineName);
+ Assert.ok(engine, `Should be able to find ${engineName}`);
+
+ let submission = engine.getSubmission("test", "text/html");
+ Assert.equal(
+ submission.uri.spec,
+ url,
+ `Should have the correct submission url for ${engineName}`
+ );
+
+ submission = engine.getSubmission("test", "application/x-suggestions+json");
+ Assert.equal(
+ submission.uri.spec,
+ suggestURL,
+ `Should have the correct suggestion url for ${engineName}`
+ );
+}
+
+function hasParams(engines, engineName, purpose, param) {
+ let engine = engines.find(e => e._name === engineName);
+ Assert.ok(engine, `Should be able to find ${engineName}`);
+
+ let submission = engine.getSubmission("test", "text/html", purpose);
+ let queries = submission.uri.query.split("&");
+
+ let paramNames = new Set();
+ for (let query of queries) {
+ let queryParam = query.split("=")[0];
+ Assert.ok(
+ !paramNames.has(queryParam),
+ `Should not have a duplicate ${queryParam} param`
+ );
+ paramNames.add(queryParam);
+ }
+
+ let result = queries.includes(param);
+ Assert.ok(result, `expect ${submission.uri.query} to include ${param}`);
+ return true;
+}
+
+function hasTelemetryId(engines, engineName, telemetryId) {
+ let engine = engines.find(e => e._name === engineName);
+ Assert.ok(engine, `Should be able to find ${engineName}`);
+
+ Assert.equal(
+ engine.telemetryId,
+ telemetryId,
+ "Should have the correct telemetryId"
+ );
+ return true;
+}
+
+function hasDefault(engines, expectedDefaultName) {
+ Assert.equal(
+ engines[0].name,
+ expectedDefaultName,
+ "Should have the expected engine set as default"
+ );
+ return true;
+}
+
+function hasEnginesFirst(engines, expectedEngines) {
+ for (let [i, expectedEngine] of expectedEngines.entries()) {
+ Assert.equal(
+ engines[i].name,
+ expectedEngine,
+ `Should have the expected engine in position ${i}`
+ );
+ }
+}
+
+engineSelector = new SearchEngineSelector();
+
+AddonTestUtils.init(GLOBAL_SCOPE);
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+
+ await maybeSetupConfig();
+});
+
+add_task(async function test_expected_distribution_engines() {
+ let searchService = new SearchService();
+ for (const { distribution, locale = "en-US", region = "US", test } of tests) {
+ let config = await engineSelector.fetchEngineConfiguration({
+ locale,
+ region,
+ distroID: distribution,
+ });
+ let engines = await SearchTestUtils.searchConfigToEngines(config.engines);
+ searchService._engines = engines;
+ searchService._searchDefault = {
+ id: config.engines[0].webExtension.id,
+ locale:
+ config.engines[0]?.webExtension?.locale ?? SearchUtils.DEFAULT_TAG,
+ };
+ engines = searchService._sortEnginesByDefaults(engines);
+ test(engines);
+ }
+});
diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_duckduckgo.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_duckduckgo.js
new file mode 100644
index 0000000000..379ef9d217
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_duckduckgo.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const test = new SearchConfigTest({
+ identifier: "ddg",
+ aliases: ["@duckduckgo", "@ddg"],
+ default: {
+ // Not included anywhere.
+ },
+ available: {
+ excluded: [
+ // Should be available everywhere.
+ ],
+ },
+ details: [
+ {
+ included: [{}],
+ domain: "duckduckgo.com",
+ telemetryId:
+ SearchUtils.MODIFIED_APP_CHANNEL == "esr" ? "ddg-esr" : "ddg",
+ searchUrlCode:
+ SearchUtils.MODIFIED_APP_CHANNEL == "esr" ? "t=ftsa" : "t=ffab",
+ },
+ ],
+});
+
+add_task(async function setup() {
+ await test.setup();
+});
+
+add_task(async function test_searchConfig_duckduckgo() {
+ await test.run();
+});
diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_ebay.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_ebay.js
new file mode 100644
index 0000000000..df89072425
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_ebay.js
@@ -0,0 +1,290 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const DOMAIN_LOCALES = {
+ "ebay-ca": ["en-CA"],
+ "ebay-ch": ["rm"],
+ "ebay-de": ["de", "dsb", "hsb"],
+ "ebay-es": ["an", "ast", "ca", "ca-valencia", "es-ES", "eu", "gl"],
+ "ebay-ie": ["ga-IE", "ie"],
+ "ebay-it": ["fur", "it", "lij", "sc"],
+ "ebay-nl": ["fy-NL", "nl"],
+ "ebay-uk": ["cy", "en-GB", "gd"],
+};
+
+const test = new SearchConfigTest({
+ identifier: "ebay",
+ aliases: ["@ebay"],
+ default: {
+ // Not included anywhere.
+ },
+ available: {
+ included: [
+ {
+ // We don't currently enforce by region, but do locale instead.
+ // regions: [
+ // "us", "gb", "ca", "ie", "fr", "it", "de", "at", "es", "nl", "ch", "au"
+ // ],
+ locales: {
+ matches: [
+ "an",
+ "ast",
+ "br",
+ "ca",
+ "ca-valencia",
+ "cy",
+ "de",
+ "dsb",
+ "en-CA",
+ "en-GB",
+ "es-ES",
+ "eu",
+ "fur",
+ "fr",
+ "fy-NL",
+ "ga-IE",
+ "gd",
+ "gl",
+ "hsb",
+ "it",
+ "lij",
+ "nl",
+ "rm",
+ "sc",
+ "wo",
+ ],
+ },
+ },
+ {
+ regions: ["au", "be", "ca", "ch", "gb", "ie", "nl", "us"],
+ locales: {
+ matches: ["en-US"],
+ },
+ },
+ {
+ regions: ["gb"],
+ locales: {
+ matches: ["sco"],
+ },
+ },
+ ],
+ },
+ suggestionUrlBase: "https://autosug.ebay.com/autosug",
+ details: [
+ {
+ // Note: These should be based on region, but we don't currently enforce that.
+ // Note: the order here is important. A region/locale match higher up in the
+ // list will override a region/locale match lower down.
+ domain: "www.befr.ebay.be",
+ telemetryId: "ebay-be",
+ included: [
+ {
+ regions: ["be"],
+ locales: {
+ matches: ["br", "unknown", "en-US", "fr", "fy-NL", "nl", "wo"],
+ },
+ },
+ ],
+ searchUrlCode: "mkrid=1553-53471-19255-0",
+ suggestUrlCode: "sId=23",
+ },
+ {
+ domain: "www.ebay.at",
+ telemetryId: "ebay-at",
+ included: [
+ {
+ regions: ["at"],
+ locales: { matches: ["de", "dsb", "hsb"] },
+ },
+ ],
+ searchUrlCode: "mkrid=5221-53469-19255-0",
+ suggestUrlCode: "sId=16",
+ },
+ {
+ domain: "www.ebay.ca",
+ telemetryId: "ebay-ca",
+ included: [
+ {
+ locales: { matches: DOMAIN_LOCALES["ebay-ca"] },
+ },
+ {
+ regions: ["ca"],
+ },
+ ],
+ excluded: [
+ {
+ locales: {
+ matches: [
+ ...DOMAIN_LOCALES["ebay-ch"],
+ ...DOMAIN_LOCALES["ebay-de"],
+ ...DOMAIN_LOCALES["ebay-es"],
+ ...DOMAIN_LOCALES["ebay-ie"],
+ ...DOMAIN_LOCALES["ebay-it"],
+ ...DOMAIN_LOCALES["ebay-nl"],
+ ...DOMAIN_LOCALES["ebay-uk"],
+ ],
+ },
+ },
+ ],
+ searchUrlCode: "mkrid=706-53473-19255-0",
+ suggestUrlCode: "sId=2",
+ },
+ {
+ domain: "www.ebay.ch",
+ telemetryId: "ebay-ch",
+ included: [
+ {
+ locales: { matches: DOMAIN_LOCALES["ebay-ch"] },
+ },
+ {
+ regions: ["ch"],
+ },
+ ],
+ excluded: [
+ {
+ locales: {
+ matches: [
+ ...DOMAIN_LOCALES["ebay-ca"],
+ ...DOMAIN_LOCALES["ebay-es"],
+ ...DOMAIN_LOCALES["ebay-ie"],
+ ...DOMAIN_LOCALES["ebay-it"],
+ ...DOMAIN_LOCALES["ebay-nl"],
+ ...DOMAIN_LOCALES["ebay-uk"],
+ ],
+ },
+ },
+ ],
+ searchUrlCode: "mkrid=5222-53480-19255-0",
+ suggestUrlCode: "sId=193",
+ },
+ {
+ domain: "www.ebay.com",
+ telemetryId: "ebay",
+ included: [
+ {
+ locales: { matches: ["unknown", "en-US"] },
+ },
+ ],
+ excluded: [{ regions: ["au", "be", "ca", "ch", "gb", "ie", "nl"] }],
+ searchUrlCode: "mkrid=711-53200-19255-0",
+ suggestUrlCode: "sId=0",
+ },
+ {
+ domain: "www.ebay.com.au",
+ telemetryId: "ebay-au",
+ included: [
+ {
+ regions: ["au"],
+ locales: { matches: ["cy", "unknown", "en-GB", "en-US", "gd"] },
+ },
+ ],
+ searchUrlCode: "mkrid=705-53470-19255-0",
+ suggestUrlCode: "sId=15",
+ },
+ {
+ domain: "www.ebay.ie",
+ telemetryId: "ebay-ie",
+ included: [
+ {
+ locales: { matches: DOMAIN_LOCALES["ebay-ie"] },
+ },
+ {
+ regions: ["ie"],
+ locales: { matches: ["cy", "unknown", "en-GB", "en-US", "gd"] },
+ },
+ ],
+ searchUrlCode: "mkrid=5282-53468-19255-0",
+ suggestUrlCode: "sId=205",
+ },
+ {
+ domain: "www.ebay.co.uk",
+ telemetryId: "ebay-uk",
+ included: [
+ {
+ locales: { matches: DOMAIN_LOCALES["ebay-uk"] },
+ },
+ {
+ locales: { matches: ["unknown", "en-US", "sco"] },
+ regions: ["gb"],
+ },
+ ],
+ excluded: [{ regions: ["au", "ie"] }],
+ searchUrlCode: "mkrid=710-53481-19255-0",
+ suggestUrlCode: "sId=3",
+ },
+ {
+ domain: "www.ebay.de",
+ telemetryId: "ebay-de",
+ included: [
+ {
+ locales: { matches: DOMAIN_LOCALES["ebay-de"] },
+ },
+ ],
+ excluded: [{ regions: ["at", "ch"] }],
+ searchUrlCode: "mkrid=707-53477-19255-0",
+ suggestUrlCode: "sId=77",
+ },
+ {
+ domain: "www.ebay.es",
+ telemetryId: "ebay-es",
+ included: [
+ {
+ locales: {
+ matches: DOMAIN_LOCALES["ebay-es"],
+ },
+ },
+ ],
+ searchUrlCode: "mkrid=1185-53479-19255-0",
+ suggestUrlCode: "sId=186",
+ },
+ {
+ domain: "www.ebay.fr",
+ telemetryId: "ebay-fr",
+ included: [
+ {
+ locales: { matches: ["br", "fr", "wo"] },
+ },
+ ],
+ excluded: [{ regions: ["be", "ca", "ch"] }],
+ searchUrlCode: "mkrid=709-53476-19255-0",
+ suggestUrlCode: "sId=71",
+ },
+ {
+ domain: "www.ebay.it",
+ telemetryId: "ebay-it",
+ included: [
+ {
+ locales: { matches: DOMAIN_LOCALES["ebay-it"] },
+ },
+ ],
+ searchUrlCode: "mkrid=724-53478-19255-0",
+ suggestUrlCode: "sId=101",
+ },
+ {
+ domain: "www.ebay.nl",
+ telemetryId: "ebay-nl",
+ included: [
+ {
+ locales: { matches: DOMAIN_LOCALES["ebay-nl"] },
+ },
+ {
+ locales: { matches: ["unknown", "en-US"] },
+ regions: ["nl"],
+ },
+ ],
+ excluded: [{ regions: ["be"] }],
+ searchUrlCode: "mkrid=1346-53482-19255-0",
+ suggestUrlCode: "sId=146",
+ },
+ ],
+});
+
+add_task(async function setup() {
+ await test.setup();
+});
+
+add_task(async function test_searchConfig_ebay() {
+ await test.run();
+});
diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_ecosia.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_ecosia.js
new file mode 100644
index 0000000000..61d2fd9abc
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_ecosia.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const test = new SearchConfigTest({
+ identifier: "ecosia",
+ aliases: [],
+ default: {
+ // Not default anywhere.
+ },
+ available: {
+ included: [
+ {
+ locales: {
+ matches: ["de"],
+ },
+ },
+ ],
+ },
+ details: [
+ {
+ included: [{}],
+ domain: "www.ecosia.org",
+ telemetryId: "ecosia",
+ searchUrlCode: "tt=mzl",
+ },
+ ],
+});
+
+add_task(async function setup() {
+ await test.setup();
+});
+
+add_task(async function test_searchConfig_ecosia() {
+ await test.run();
+});
diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_google.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_google.js
new file mode 100644
index 0000000000..dc2098b066
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_google.js
@@ -0,0 +1,173 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+});
+
+const test = new SearchConfigTest({
+ identifier: "google",
+ aliases: ["@google"],
+ default: {
+ // Included everywhere apart from the exclusions below. These are basically
+ // just excluding what Yandex and Baidu include.
+ excluded: [
+ {
+ regions: ["cn"],
+ locales: {
+ matches: ["zh-CN"],
+ },
+ },
+ ],
+ },
+ available: {
+ excluded: [
+ // Should be available everywhere.
+ ],
+ },
+ details: [
+ {
+ included: [{ regions: ["us"] }],
+ domain: "google.com",
+ telemetryId:
+ SearchUtils.MODIFIED_APP_CHANNEL == "esr"
+ ? "google-b-1-e"
+ : "google-b-1-d",
+ codes:
+ SearchUtils.MODIFIED_APP_CHANNEL == "esr"
+ ? "client=firefox-b-1-e"
+ : "client=firefox-b-1-d",
+ },
+ {
+ excluded: [{ regions: ["us", "by", "kz", "ru", "tr"] }],
+ included: [{}],
+ domain: "google.com",
+ telemetryId:
+ SearchUtils.MODIFIED_APP_CHANNEL == "esr" ? "google-b-e" : "google-b-d",
+ codes:
+ SearchUtils.MODIFIED_APP_CHANNEL == "esr"
+ ? "client=firefox-b-e"
+ : "client=firefox-b-d",
+ },
+ {
+ included: [{ regions: ["by", "kz", "ru", "tr"] }],
+ domain: "google.com",
+ telemetryId: "google-com-nocodes",
+ },
+ ],
+});
+
+add_task(async function setup() {
+ sinon.spy(NimbusFeatures.search, "onUpdate");
+ sinon.stub(NimbusFeatures.search, "ready").resolves();
+ await test.setup();
+});
+
+add_task(async function test_searchConfig_google() {
+ await test.run();
+});
+
+add_task(async function test_searchConfig_google_with_mozparam() {
+ // Test a couple of configurations with a MozParam set up.
+ const TEST_DATA = [
+ {
+ locale: "en-US",
+ region: "US",
+ pref: "google_channel_us",
+ expected: "us_param",
+ },
+ {
+ locale: "en-US",
+ region: "GB",
+ pref: "google_channel_row",
+ expected: "row_param",
+ },
+ ];
+
+ const defaultBranch = Services.prefs.getDefaultBranch(
+ SearchUtils.BROWSER_SEARCH_PREF
+ );
+ for (const testData of TEST_DATA) {
+ defaultBranch.setCharPref("param." + testData.pref, testData.expected);
+ }
+
+ for (const testData of TEST_DATA) {
+ info(`Checking region ${testData.region}, locale ${testData.locale}`);
+ const engines = await test._getEngines(testData.region, testData.locale);
+
+ Assert.ok(
+ engines[0].identifier.startsWith("google"),
+ "Should have the correct engine"
+ );
+ console.log(engines[0]);
+
+ const submission = engines[0].getSubmission("test", URLTYPE_SEARCH_HTML);
+ Assert.ok(
+ submission.uri.query.split("&").includes("channel=" + testData.expected),
+ "Should be including the correct MozParam parameter for the engine"
+ );
+ }
+
+ // Reset the pref values for next tests
+ for (const testData of TEST_DATA) {
+ defaultBranch.setCharPref("param." + testData.pref, "");
+ }
+});
+
+add_task(async function test_searchConfig_google_with_nimbus() {
+ let sandbox = sinon.createSandbox();
+ // Test a couple of configurations with a MozParam set up.
+ const TEST_DATA = [
+ {
+ locale: "en-US",
+ region: "US",
+ expected: "nimbus_us_param",
+ },
+ {
+ locale: "en-US",
+ region: "GB",
+ expected: "nimbus_row_param",
+ },
+ ];
+
+ Assert.ok(
+ NimbusFeatures.search.onUpdate.called,
+ "Should register an update listener for Nimbus experiments"
+ );
+ // Stub getVariable to populate the cache with our expected data
+ sandbox.stub(NimbusFeatures.search, "getVariable").returns([
+ { key: "google_channel_us", value: "nimbus_us_param" },
+ { key: "google_channel_row", value: "nimbus_row_param" },
+ ]);
+ // Set the pref cache with Nimbus values
+ NimbusFeatures.search.onUpdate.firstCall.args[0]();
+
+ for (const testData of TEST_DATA) {
+ info(`Checking region ${testData.region}, locale ${testData.locale}`);
+ const engines = await test._getEngines(testData.region, testData.locale);
+
+ Assert.ok(
+ engines[0].identifier.startsWith("google"),
+ "Should have the correct engine"
+ );
+ console.log(engines[0]);
+
+ const submission = engines[0].getSubmission("test", URLTYPE_SEARCH_HTML);
+ Assert.ok(
+ NimbusFeatures.search.ready.called,
+ "Should wait for Nimbus to get ready"
+ );
+ Assert.ok(
+ NimbusFeatures.search.getVariable,
+ "Should call NimbusFeatures.search.getVariable to populate the cache"
+ );
+ Assert.ok(
+ submission.uri.query.split("&").includes("channel=" + testData.expected),
+ "Should be including the correct MozParam parameter for the engine"
+ );
+ }
+
+ sandbox.restore();
+});
diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_mailru.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_mailru.js
new file mode 100644
index 0000000000..4d413f0b5b
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_mailru.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const test = new SearchConfigTest({
+ identifier: "mailru",
+ aliases: [],
+ default: {
+ // Not default anywhere.
+ },
+ available: {
+ included: [
+ {
+ locales: {
+ matches: ["ru"],
+ },
+ },
+ ],
+ },
+ details: [
+ {
+ included: [{}],
+ domain: "go.mail.ru",
+ telemetryId: "mailru",
+ codes: "gp=900200",
+ searchUrlCode: "frc=900200",
+ },
+ ],
+});
+
+add_task(async function setup() {
+ await test.setup();
+});
+
+add_task(async function test_searchConfig_mailru() {
+ await test.run();
+}).skip();
diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_qwant.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_qwant.js
new file mode 100644
index 0000000000..8db31fcc24
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_qwant.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const test = new SearchConfigTest({
+ identifier: "qwant",
+ aliases: ["@qwant"],
+ default: {
+ // Not default anywhere.
+ },
+ available: {
+ included: [
+ {
+ locales: {
+ matches: ["fr"],
+ },
+ },
+ ],
+ },
+ details: [
+ {
+ included: [{}],
+ domain: "www.qwant.com",
+ telemetryId: "qwant",
+ searchUrlCode: "client=brz-moz",
+ suggestUrlCode: "client=opensearch",
+ },
+ ],
+});
+
+add_task(async function setup() {
+ await test.setup();
+});
+
+add_task(async function test_searchConfig_qwant() {
+ await test.run();
+});
diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_rakuten.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_rakuten.js
new file mode 100644
index 0000000000..0577490dc2
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_rakuten.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const test = new SearchConfigTest({
+ identifier: "rakuten",
+ aliases: [],
+ default: {
+ // Not default anywhere.
+ },
+ available: {
+ included: [
+ {
+ locales: {
+ matches: ["ja", "ja-JP-macos"],
+ },
+ },
+ ],
+ },
+ details: [
+ {
+ included: [{}],
+ domain: "rakuten.co.jp",
+ telemetryId: "rakuten",
+ searchUrlCodeNotInQuery: "013ca98b.cd7c5f0c",
+ },
+ ],
+});
+
+add_task(async function setup() {
+ await test.setup();
+});
+
+add_task(async function test_searchConfig_rakuten() {
+ await test.run();
+});
diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_selector_db_out_of_date.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_selector_db_out_of_date.js
new file mode 100644
index 0000000000..742cfaec8a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_selector_db_out_of_date.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ RemoteSettingsWorker:
+ "resource://services-settings/RemoteSettingsWorker.sys.mjs",
+ SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs",
+});
+
+do_get_profile();
+
+add_task(async function test_selector_db_out_of_date() {
+ let searchConfig = RemoteSettings(SearchUtils.SETTINGS_KEY);
+
+ // Do an initial get to pre-seed the database.
+ await searchConfig.get();
+
+ // Now clear the database and re-fill it.
+ let db = searchConfig.db;
+ await db.clear();
+ let databaseEntries = await db.list();
+ Assert.equal(databaseEntries.length, 0, "Should have cleared the database.");
+
+ // Add a dummy record with an out-of-date last modified.
+ await RemoteSettingsWorker._execute("_test_only_import", [
+ "main",
+ SearchUtils.SETTINGS_KEY,
+ [
+ {
+ id: "b70edfdd-1c3f-4b7b-ab55-38cb048636c0",
+ default: "yes",
+ webExtension: { id: "outofdate@search.mozilla.org" },
+ appliesTo: [{ included: { everywhere: true } }],
+ last_modified: 1606227264000,
+ },
+ ],
+ 1606227264000,
+ ]);
+
+ // Now load the configuration and check we get what we expect.
+ let engineSelector = new SearchEngineSelector();
+ let result = await engineSelector.fetchEngineConfiguration({
+ // Use the fallback default locale/regions to get a simple list.
+ locale: "default",
+ region: "default",
+ });
+ Assert.deepEqual(
+ result.engines.map(e => e.webExtension.id),
+ [
+ "google@search.mozilla.org",
+ "wikipedia@search.mozilla.org",
+ "ddg@search.mozilla.org",
+ ],
+ "Should have returned the correct data."
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_yahoojp.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_yahoojp.js
new file mode 100644
index 0000000000..25091fc37e
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_yahoojp.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const test = new SearchConfigTest({
+ identifier: "yahoo-jp",
+ identifierExactMatch: true,
+ aliases: [],
+ default: {
+ // Not default anywhere.
+ },
+ available: {
+ included: [
+ {
+ locales: {
+ matches: ["ja", "ja-JP-macos"],
+ },
+ },
+ ],
+ },
+ details: [
+ {
+ included: [{}],
+ domain: "search.yahoo.co.jp",
+ telemetryId: "yahoo-jp",
+ searchUrlCode: "fr=mozff",
+ },
+ ],
+});
+
+add_task(async function setup() {
+ await test.setup();
+});
+
+add_task(async function test_searchConfig_yahoojp() {
+ await test.run();
+});
diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_yandex.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_yandex.js
new file mode 100644
index 0000000000..c315165e5f
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_yandex.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const test = new SearchConfigTest({
+ identifier: "yandex",
+ aliases: ["@\u044F\u043D\u0434\u0435\u043A\u0441", "@yandex"],
+ default: {
+ included: [
+ {
+ regions: ["ru", "tr", "by", "kz"],
+ locales: {
+ matches: ["ru", "tr", "be", "kk"],
+ startsWith: ["en"],
+ },
+ },
+ ],
+ },
+ available: {
+ included: [
+ {
+ locales: {
+ matches: ["az", "ru", "be", "kk", "tr"],
+ },
+ },
+ {
+ regions: ["ru", "tr", "by", "kz"],
+ locales: {
+ startsWith: ["en"],
+ },
+ },
+ ],
+ },
+ details: [
+ {
+ included: [{ locales: { matches: ["az"] } }],
+ domain: "yandex.az",
+ telemetryId: "yandex-az",
+ codes: {
+ searchbar: "clid=2186618",
+ keyword: "clid=2186621",
+ contextmenu: "clid=2186623",
+ homepage: "clid=2186617",
+ newtab: "clid=2186620",
+ },
+ },
+ {
+ included: [{ locales: { startsWith: ["en"] } }],
+ domain: "yandex.com",
+ telemetryId: "yandex-en",
+ codes: {
+ searchbar: "clid=2186618",
+ keyword: "clid=2186621",
+ contextmenu: "clid=2186623",
+ homepage: "clid=2186617",
+ newtab: "clid=2186620",
+ },
+ },
+ {
+ included: [{ locales: { matches: ["ru"] } }],
+ domain: "yandex.ru",
+ telemetryId: "yandex-ru",
+ codes: {
+ searchbar: "clid=2186618",
+ keyword: "clid=2186621",
+ contextmenu: "clid=2186623",
+ homepage: "clid=2186617",
+ newtab: "clid=2186620",
+ },
+ },
+ {
+ included: [{ locales: { matches: ["be"] } }],
+ domain: "yandex.by",
+ telemetryId: "yandex-by",
+ codes: {
+ searchbar: "clid=2186618",
+ keyword: "clid=2186621",
+ contextmenu: "clid=2186623",
+ homepage: "clid=2186617",
+ newtab: "clid=2186620",
+ },
+ },
+ {
+ included: [{ locales: { matches: ["kk"] } }],
+ domain: "yandex.kz",
+ telemetryId: "yandex-kk",
+ codes: {
+ searchbar: "clid=2186618",
+ keyword: "clid=2186621",
+ contextmenu: "clid=2186623",
+ homepage: "clid=2186617",
+ newtab: "clid=2186620",
+ },
+ },
+ {
+ included: [{ locales: { matches: ["tr"] } }],
+ domain: "yandex.com.tr",
+ telemetryId: "yandex-tr",
+ codes: {
+ searchbar: "clid=2186618",
+ keyword: "clid=2186621",
+ contextmenu: "clid=2186623",
+ homepage: "clid=2186617",
+ newtab: "clid=2186620",
+ },
+ },
+ ],
+});
+
+add_task(async function setup() {
+ await test.setup();
+});
+
+add_task(async function test_searchConfig_yandex() {
+ await test.run();
+}).skip();
diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/xpcshell.ini b/toolkit/components/search/tests/xpcshell/searchconfigs/xpcshell.ini
new file mode 100644
index 0000000000..17cbd5350d
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/xpcshell.ini
@@ -0,0 +1,34 @@
+[DEFAULT]
+firefox-appdir = browser
+head = head_searchconfig.js
+dupe-manifest =
+support-files =
+ ../../../../../../browser/locales/all-locales
+tags=searchconfig remote-settings
+# These are extensive tests, we don't need to run them on asan/tsan.
+# They are also skipped for mobile and Thunderbird as these are specifically
+# testing the Firefox config at the moment.
+skip-if =
+ toolkit == 'android'
+ appname == "thunderbird"
+ asan
+ tsan
+ debug
+ (os == "win" && ccov)
+# These tests do take a little longer on Linux ccov, so allow that here.
+requesttimeoutfactor = 2
+
+[test_amazon.js]
+[test_baidu.js]
+[test_bing.js]
+[test_distributions.js]
+[test_duckduckgo.js]
+[test_ebay.js]
+[test_ecosia.js]
+[test_google.js]
+[test_mailru.js]
+[test_qwant.js]
+[test_rakuten.js]
+[test_selector_db_out_of_date.js]
+[test_yahoojp.js]
+[test_yandex.js]
diff --git a/toolkit/components/search/tests/xpcshell/simple-engines/basic/manifest.json b/toolkit/components/search/tests/xpcshell/simple-engines/basic/manifest.json
new file mode 100644
index 0000000000..b62eb9bb2b
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/simple-engines/basic/manifest.json
@@ -0,0 +1,29 @@
+{
+ "name": "basic",
+ "manifest_version": 2,
+ "version": "1.0",
+ "description": "basic",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "basic@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "basic",
+ "search_url": "https://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB",
+ "params": [
+ {
+ "name": "search",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "sourceId",
+ "value": "Mozilla-search"
+ }
+ ],
+ "suggest_url": "https://ar.wikipedia.org/w/api.php?action=opensearch&search={searchTerms}"
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/simple-engines/engines.json b/toolkit/components/search/tests/xpcshell/simple-engines/engines.json
new file mode 100644
index 0000000000..f4ce227e9f
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/simple-engines/engines.json
@@ -0,0 +1,27 @@
+{
+ "data": [
+ {
+ "webExtension": {
+ "id": "basic@search.mozilla.org"
+ },
+ "telemetryId": "telemetry",
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "default": "yes"
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "simple@search.mozilla.org"
+ },
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "default": "yes"
+ }
+ ]
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/simple-engines/hidden/manifest.json b/toolkit/components/search/tests/xpcshell/simple-engines/hidden/manifest.json
new file mode 100644
index 0000000000..203590f44d
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/simple-engines/hidden/manifest.json
@@ -0,0 +1,29 @@
+{
+ "name": "hidden",
+ "manifest_version": 2,
+ "version": "1.0",
+ "description": "Hidden engine to test bug 1194265",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "hidden@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "hidden",
+ "search_url": "https://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB",
+ "params": [
+ {
+ "name": "search",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "sourceId",
+ "value": "Mozilla-search"
+ }
+ ],
+ "suggest_url": "https://ar.wikipedia.org/w/api.php?action=opensearch&search={searchTerms}"
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/simple-engines/simple/manifest.json b/toolkit/components/search/tests/xpcshell/simple-engines/simple/manifest.json
new file mode 100644
index 0000000000..67d2974753
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/simple-engines/simple/manifest.json
@@ -0,0 +1,29 @@
+{
+ "name": "Simple Engine",
+ "manifest_version": 2,
+ "version": "1.0",
+ "description": "Simple engine with a different name from the WebExtension id prefix",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "simple@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "Simple Engine",
+ "search_url": "https://example.com",
+ "params": [
+ {
+ "name": "sourceId",
+ "value": "Mozilla-search"
+ },
+ {
+ "name": "search",
+ "value": "{searchTerms}"
+ }
+ ],
+ "suggest_url": "https://example.com?search={searchTerms}"
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/engines.json b/toolkit/components/search/tests/xpcshell/test-extensions/engines.json
new file mode 100644
index 0000000000..fcb5e03e82
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test-extensions/engines.json
@@ -0,0 +1,58 @@
+{
+ "data": [
+ {
+ "webExtension": {
+ "id": "plainengine@search.mozilla.org"
+ },
+ "orderHint": 10000,
+ "sendAttributionRequest": true,
+ "appliesTo": [
+ {
+ "included": { "everywhere": true },
+ "default": "yes-if-no-other"
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "special-engine@search.mozilla.org"
+ },
+ "orderHint": 7000,
+ "appliesTo": [
+ {
+ "included": { "regions": ["tr"] },
+ "default": "yes"
+ },
+ {
+ "included": { "everywhere": true },
+ "sendAttributionRequest": true
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "multilocale@search.mozilla.org",
+ "locales": ["an"]
+ },
+ "orderHint": 6000,
+ "appliesTo": [
+ {
+ "included": { "regions": ["an"] },
+ "default": "yes"
+ }
+ ]
+ },
+ {
+ "webExtension": {
+ "id": "multilocale@search.mozilla.org",
+ "locales": ["af", "an"]
+ },
+ "orderHint": 6500,
+ "appliesTo": [
+ {
+ "included": { "regions": ["af"] }
+ }
+ ]
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/_locales/af/messages.json b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/_locales/af/messages.json
new file mode 100644
index 0000000000..95e49f9bc5
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/_locales/af/messages.json
@@ -0,0 +1,20 @@
+{
+ "extensionName": {
+ "message": "Multilocale AF"
+ },
+ "extensionDescription": {
+ "message": "Wikipedia, die vrye ensiklopedie"
+ },
+ "url_lang": {
+ "message": "af"
+ },
+ "searchUrl": {
+ "message": "https://af.wikipedia.org/wiki/Spesiaal:Soek"
+ },
+ "suggestUrl": {
+ "message": "https://af.wikipedia.org/w/api.php"
+ },
+ "extensionIcon": {
+ "message": "favicon-af.ico"
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/_locales/an/messages.json b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/_locales/an/messages.json
new file mode 100644
index 0000000000..6222338596
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/_locales/an/messages.json
@@ -0,0 +1,20 @@
+{
+ "extensionName": {
+ "message": "Multilocale AN"
+ },
+ "extensionDescription": {
+ "message": "A enciclopedia Libre"
+ },
+ "url_lang": {
+ "message": "an"
+ },
+ "searchUrl": {
+ "message": "https://an.wikipedia.org/wiki/Especial:Mirar"
+ },
+ "suggestUrl": {
+ "message": "https://an.wikipedia.org/w/api.php"
+ },
+ "extensionIcon": {
+ "message": "favicon-an.ico"
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/favicon-af.ico b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/favicon-af.ico
new file mode 100644
index 0000000000..4314071e24
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/favicon-af.ico
Binary files differ
diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/favicon-an.ico b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/favicon-an.ico
new file mode 100644
index 0000000000..dda80dfd88
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/favicon-an.ico
Binary files differ
diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/manifest.json b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/manifest.json
new file mode 100644
index 0000000000..a117ffb0db
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/manifest.json
@@ -0,0 +1,23 @@
+{
+ "name": "__MSG_extensionName__",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "multilocale@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "description": "__MSG_extensionDescription__",
+ "icons": {
+ "16": "__MSG_extensionIcon__"
+ },
+ "default_locale": "af",
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "__MSG_extensionName__",
+ "search_url": "__MSG_searchUrl__",
+ "suggest_url": "__MSG_searchUrl__"
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/plainengine/favicon.ico b/toolkit/components/search/tests/xpcshell/test-extensions/plainengine/favicon.ico
new file mode 100644
index 0000000000..dda80dfd88
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test-extensions/plainengine/favicon.ico
Binary files differ
diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/plainengine/manifest.json b/toolkit/components/search/tests/xpcshell/test-extensions/plainengine/manifest.json
new file mode 100644
index 0000000000..cabb4c9f9a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test-extensions/plainengine/manifest.json
@@ -0,0 +1,58 @@
+{
+ "name": "Plain",
+ "description": "Plain Engine",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "plainengine@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "icons": {
+ "16": "favicon.ico"
+ },
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "Plain",
+ "search_url": "https://duckduckgo.com/",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "t",
+ "condition": "purpose",
+ "purpose": "contextmenu",
+ "value": "ffcm"
+ },
+ {
+ "name": "t",
+ "condition": "purpose",
+ "purpose": "keyword",
+ "value": "ffab"
+ },
+ {
+ "name": "t",
+ "condition": "purpose",
+ "purpose": "searchbar",
+ "value": "ffsb"
+ },
+ {
+ "name": "t",
+ "condition": "purpose",
+ "purpose": "homepage",
+ "value": "ffhp"
+ },
+ {
+ "name": "t",
+ "condition": "purpose",
+ "purpose": "newtab",
+ "value": "ffnt"
+ }
+ ],
+ "suggest_url": "https://ac.duckduckgo.com/ac/q={searchTerms}&type=list"
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/special-engine/favicon.ico b/toolkit/components/search/tests/xpcshell/test-extensions/special-engine/favicon.ico
new file mode 100644
index 0000000000..82339b3b1d
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test-extensions/special-engine/favicon.ico
Binary files differ
diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/special-engine/manifest.json b/toolkit/components/search/tests/xpcshell/test-extensions/special-engine/manifest.json
new file mode 100644
index 0000000000..1568c6ed55
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test-extensions/special-engine/manifest.json
@@ -0,0 +1,40 @@
+{
+ "name": "Special",
+ "description": "Special Engine",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "special-engine@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "icons": {
+ "16": "favicon.ico"
+ },
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "Special",
+ "search_url": "https://www.google.com/search",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "client",
+ "condition": "purpose",
+ "purpose": "keyword",
+ "value": "firefox-b-1-ab"
+ },
+ {
+ "name": "client",
+ "condition": "purpose",
+ "purpose": "searchbar",
+ "value": "firefox-b-1"
+ }
+ ],
+ "suggest_url": "https://www.google.com/complete/search?client=firefox&q={searchTerms}"
+ }
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_SearchStaticData.js b/toolkit/components/search/tests/xpcshell/test_SearchStaticData.js
new file mode 100644
index 0000000000..741953a1cf
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_SearchStaticData.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the SearchStaticData module.
+ */
+
+"use strict";
+
+const { SearchStaticData } = ChromeUtils.importESModule(
+ "resource://gre/modules/SearchStaticData.sys.mjs"
+);
+
+function run_test() {
+ Assert.ok(
+ SearchStaticData.getAlternateDomains("www.google.com").includes(
+ "www.google.fr"
+ )
+ );
+ Assert.ok(
+ SearchStaticData.getAlternateDomains("www.google.fr").includes(
+ "www.google.com"
+ )
+ );
+ Assert.ok(
+ SearchStaticData.getAlternateDomains("www.google.com").every(d =>
+ d.startsWith("www.google.")
+ )
+ );
+ Assert.ok(!SearchStaticData.getAlternateDomains("google.com").length);
+
+ // Test that methods from SearchStaticData module can be overwritten,
+ // needed for hotfixing.
+ let backup = SearchStaticData.getAlternateDomains;
+ SearchStaticData.getAlternateDomains = () => ["www.bing.fr"];
+ Assert.deepEqual(SearchStaticData.getAlternateDomains("www.bing.com"), [
+ "www.bing.fr",
+ ]);
+ SearchStaticData.getAlternateDomains = backup;
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_appDefaultEngine.js b/toolkit/components/search/tests/xpcshell/test_appDefaultEngine.js
new file mode 100644
index 0000000000..0b6fa30fdf
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_appDefaultEngine.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test that appDefaultEngine property is set and switches correctly.
+ */
+
+"use strict";
+
+add_task(async function setup() {
+ Region._setHomeRegion("an", false);
+ await AddonTestUtils.promiseStartupManager();
+ await SearchTestUtils.useTestEngines("test-extensions");
+});
+
+add_task(async function test_appDefaultEngine() {
+ await Promise.all([Services.search.init(), promiseAfterSettings()]);
+ Assert.equal(
+ Services.search.appDefaultEngine.name,
+ "Multilocale AN",
+ "Should have returned the correct app default engine"
+ );
+});
+
+add_task(async function test_changeRegion() {
+ // Now change the region, and check we get the correct default according to
+ // the config file.
+
+ // Note: the test could be done with changing regions or locales. The important
+ // part is that the default engine is changing across the switch, and that
+ // the engine is not the first one in the new sorted engines list.
+ await promiseSetHomeRegion("tr");
+
+ Assert.equal(
+ Services.search.appDefaultEngine.name,
+ // Very important this default is not the first one in the list (which is
+ // the next fallback if the config one can't be found).
+ "Special",
+ "Should have returned the correct engine for the new locale"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_async.js b/toolkit/components/search/tests/xpcshell/test_async.js
new file mode 100644
index 0000000000..f00ba02755
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_async.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+ await SearchTestUtils.useTestEngines("simple-engines");
+ Services.fog.initializeFOG();
+});
+
+add_task(async function test_async() {
+ Assert.ok(!Services.search.isInitialized);
+
+ let aStatus = await Services.search.init();
+ Assert.ok(Components.isSuccessCode(aStatus));
+ Assert.ok(Services.search.isInitialized);
+
+ // test engines from dir are not loaded.
+ let engines = await Services.search.getEngines();
+ Assert.equal(engines.length, 2);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("basic");
+ Assert.notEqual(engine, null);
+ Assert.ok(engine.isAppProvided, "Should be shown as an app-provided engine");
+
+ engine = Services.search.getEngineByName("Simple Engine");
+ Assert.notEqual(engine, null);
+ Assert.ok(engine.isAppProvided, "Should be shown as an app-provided engine");
+
+ // Check the hidden engine is not loaded.
+ engine = Services.search.getEngineByName("hidden");
+ Assert.equal(engine, null);
+
+ // Check if there is a value for startup_time
+ Assert.notEqual(
+ await Glean.searchService.startupTime.testGetValue(),
+ undefined,
+ "Should have a value stored in startup_time"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_config_attribution.js b/toolkit/components/search/tests/xpcshell/test_config_attribution.js
new file mode 100644
index 0000000000..9996cc83c4
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_config_attribution.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function setup() {
+ Region._setHomeRegion("an", false);
+ await SearchTestUtils.useTestEngines("test-extensions");
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+});
+
+add_task(async function test_send_attribution_request() {
+ let engine = await Services.search.getEngineByName("Plain");
+ Assert.ok(
+ engine.sendAttributionRequest,
+ "Should have noted to send the attribution request for Plain"
+ );
+
+ engine = await Services.search.getEngineByName("Special");
+ Assert.ok(
+ engine.sendAttributionRequest,
+ "Should have noted to send the attribution request for Special"
+ );
+
+ engine = await Services.search.getEngineByName("Multilocale AN");
+ Assert.ok(
+ !engine.sendAttributionRequest,
+ "Should not have noted to send the attribution request for Multilocale AN"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_config_engine_params.js b/toolkit/components/search/tests/xpcshell/test_config_engine_params.js
new file mode 100644
index 0000000000..795dc56324
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_config_engine_params.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("method-extensions");
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+});
+
+add_task(async function test_get_extension() {
+ let engine = Services.search.getEngineByName("Get Engine");
+ Assert.notEqual(engine, null, "Should have found an engine");
+
+ let url = engine.wrappedJSObject._getURLOfType(SearchUtils.URL_TYPE.SEARCH);
+ Assert.equal(url.method, "GET", "Search URLs method is GET");
+
+ let submission = engine.getSubmission("foo");
+ Assert.equal(
+ submission.uri.spec,
+ "https://example.com/?config=1&search=foo",
+ "Search URLs should match"
+ );
+
+ let submissionSuggest = engine.getSubmission(
+ "bar",
+ SearchUtils.URL_TYPE.SUGGEST_JSON
+ );
+ Assert.equal(
+ submissionSuggest.uri.spec,
+ "https://example.com/?config=1&suggest=bar",
+ "Suggest URLs should match"
+ );
+});
+
+add_task(async function test_post_extension() {
+ let engine = Services.search.getEngineByName("Post Engine");
+ Assert.ok(!!engine, "Should have found an engine");
+
+ let url = engine.wrappedJSObject._getURLOfType(SearchUtils.URL_TYPE.SEARCH);
+ Assert.equal(url.method, "POST", "Search URLs method is POST");
+
+ let submission = engine.getSubmission("foo");
+ Assert.equal(
+ submission.uri.spec,
+ "https://example.com/",
+ "Search URLs should match"
+ );
+ Assert.equal(
+ submission.postData.data.data,
+ "config=1&search=foo",
+ "Search postData should match"
+ );
+
+ let submissionSuggest = engine.getSubmission(
+ "bar",
+ SearchUtils.URL_TYPE.SUGGEST_JSON
+ );
+ Assert.equal(
+ submissionSuggest.uri.spec,
+ "https://example.com/",
+ "Suggest URLs should match"
+ );
+ Assert.equal(
+ submissionSuggest.postData.data.data,
+ "config=1&suggest=bar",
+ "Suggest postData should match"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_defaultEngine.js b/toolkit/components/search/tests/xpcshell/test_defaultEngine.js
new file mode 100644
index 0000000000..24b7b2102a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_defaultEngine.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test that defaultEngine property can be set and yields the proper events and\
+ * behavior (search results)
+ */
+
+"use strict";
+
+let engine1;
+let engine2;
+
+add_setup(async () => {
+ do_get_profile();
+ Services.fog.initializeFOG();
+
+ useHttpServer();
+ await AddonTestUtils.promiseStartupManager();
+
+ await Services.search.init();
+
+ engine1 = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engine.xml`,
+ });
+ engine2 = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engine2.xml`,
+ });
+});
+
+function promiseDefaultNotification() {
+ return SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.DEFAULT,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+}
+
+add_task(async function test_defaultEngine() {
+ let promise = promiseDefaultNotification();
+ Services.search.defaultEngine = engine1;
+ Assert.equal((await promise).wrappedJSObject, engine1);
+ Assert.equal(Services.search.defaultEngine.wrappedJSObject, engine1);
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "other-Test search engine",
+ displayName: "Test search engine",
+ loadPath: "[http]localhost/test-search-engine.xml",
+ submissionUrl: "https://www.google.com/search?q=",
+ verified: "verified",
+ },
+ });
+
+ promise = promiseDefaultNotification();
+ Services.search.defaultEngine = engine2;
+ Assert.equal((await promise).wrappedJSObject, engine2);
+ Assert.equal(Services.search.defaultEngine.wrappedJSObject, engine2);
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "other-A second test engine",
+ displayName: "A second test engine",
+ loadPath: "[http]localhost/a-second-test-engine.xml",
+ submissionUrl: "https://duckduckgo.com/?q=",
+ verified: "verified",
+ },
+ });
+
+ promise = promiseDefaultNotification();
+ Services.search.defaultEngine = engine1;
+ Assert.equal((await promise).wrappedJSObject, engine1);
+ Assert.equal(Services.search.defaultEngine.wrappedJSObject, engine1);
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "other-Test search engine",
+ displayName: "Test search engine",
+ loadPath: "[http]localhost/test-search-engine.xml",
+ submissionUrl: "https://www.google.com/search?q=",
+ verified: "verified",
+ },
+ });
+});
+
+add_task(async function test_telemetry_empty_submission_url() {
+ let engine = await Services.search.addOpenSearchEngine(
+ gDataUrl + "../opensearch/simple.xml",
+ null
+ );
+ Services.search.defaultPrivateEngine = engine;
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "other-simple",
+ displayName: "simple",
+ loadPath: "[http]localhost/simple.xml",
+ submissionUrl: "blank:",
+ verified: "verified",
+ },
+ private: {
+ engineId: "",
+ displayName: "",
+ loadPath: "",
+ submissionUrl: "blank:",
+ verified: "",
+ },
+ });
+});
+
+add_task(async function test_switch_with_invalid_overriddenBy() {
+ engine1.wrappedJSObject.setAttr("overriddenBy", "random@id");
+
+ consoleAllowList.push(
+ "Test search engine had overriddenBy set, but no _overriddenData"
+ );
+
+ let promise = promiseDefaultNotification();
+ Services.search.defaultEngine = engine2;
+ Assert.equal((await promise).wrappedJSObject, engine2);
+ Assert.equal(Services.search.defaultEngine.wrappedJSObject, engine2);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_defaultEngine_experiments.js b/toolkit/components/search/tests/xpcshell/test_defaultEngine_experiments.js
new file mode 100644
index 0000000000..59cd0c0942
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_defaultEngine_experiments.js
@@ -0,0 +1,422 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test that defaultEngine property can be set and yields the proper events and\
+ * behavior (search results)
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+});
+
+let getVariableStub;
+
+let defaultGetVariable = name => {
+ if (name == "seperatePrivateDefaultUIEnabled") {
+ return true;
+ }
+ if (name == "seperatePrivateDefaultUrlbarResultEnabled") {
+ return false;
+ }
+ return undefined;
+};
+
+add_setup(async () => {
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ true
+ );
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+
+ sinon.spy(NimbusFeatures.searchConfiguration, "onUpdate");
+ sinon.stub(NimbusFeatures.searchConfiguration, "ready").resolves();
+ getVariableStub = sinon.stub(
+ NimbusFeatures.searchConfiguration,
+ "getVariable"
+ );
+ getVariableStub.callsFake(defaultGetVariable);
+
+ do_get_profile();
+ Services.fog.initializeFOG();
+
+ await SearchTestUtils.useTestEngines("data1");
+
+ await AddonTestUtils.promiseStartupManager();
+
+ let promiseSaved = promiseSaveSettingsData();
+ await Services.search.init();
+ await promiseSaved;
+});
+
+async function switchExperiment(newExperiment) {
+ let promiseReloaded =
+ SearchTestUtils.promiseSearchNotification("engines-reloaded");
+ let promiseSaved = promiseSaveSettingsData();
+
+ // Stub getVariable to populate the cache with our expected data
+ getVariableStub.callsFake(name => {
+ if (name == "experiment") {
+ return newExperiment;
+ }
+ return defaultGetVariable(name);
+ });
+ for (let call of NimbusFeatures.searchConfiguration.onUpdate.getCalls()) {
+ call.args[0]();
+ }
+
+ await promiseReloaded;
+ await promiseSaved;
+}
+
+function getSettingsAttribute(setting) {
+ return Services.search.wrappedJSObject._settings.getVerifiedMetaDataAttribute(
+ setting
+ );
+}
+
+add_task(async function test_experiment_setting() {
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine1",
+ "Should have the application default engine as default"
+ );
+
+ // Start the experiment.
+ await switchExperiment("exp1");
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine2",
+ "Should have set the experiment engine as default"
+ );
+
+ // End the experiment.
+ await switchExperiment("");
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine1",
+ "Should have reset the default engine to the application default"
+ );
+ Assert.equal(
+ getSettingsAttribute("defaultEngineId"),
+ "",
+ "Should have kept the saved attribute as empty"
+ );
+});
+
+add_task(async function test_experiment_setting_to_same_as_user() {
+ Services.search.defaultEngine = Services.search.getEngineByName("engine2");
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine2",
+ "Should have the user selected engine as default"
+ );
+ Assert.equal(
+ getSettingsAttribute("defaultEngineId"),
+ "engine2@search.mozilla.orgdefault"
+ );
+
+ // Start the experiment, ensure user default is maintained.
+ await switchExperiment("exp1");
+ Assert.equal(
+ getSettingsAttribute("defaultEngineId"),
+ "engine2@search.mozilla.orgdefault"
+ );
+
+ Assert.equal(
+ Services.search.appDefaultEngine.name,
+ "engine2",
+ "Should have set the experiment engine as default"
+ );
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine2",
+ "Should have set the experiment engine as default"
+ );
+
+ // End the experiment.
+ await switchExperiment("");
+
+ Assert.equal(
+ Services.search.appDefaultEngine.name,
+ "engine1",
+ "Should have set the app default engine correctly"
+ );
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine2",
+ "Should have kept the engine the same "
+ );
+ Assert.equal(
+ getSettingsAttribute("defaultEngineId"),
+ "engine2@search.mozilla.orgdefault",
+ "Should have kept the saved attribute as the user's preference"
+ );
+});
+
+add_task(async function test_experiment_setting_user_changed_back_during() {
+ Services.search.defaultEngine = Services.search.getEngineByName("engine1");
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine1",
+ "Should have the application default engine as default"
+ );
+ Assert.equal(
+ getSettingsAttribute("defaultEngineId"),
+ "",
+ "Should have an empty settings attribute"
+ );
+
+ // Start the experiment.
+ await switchExperiment("exp1");
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine2",
+ "Should have set the experiment engine as default"
+ );
+ Assert.equal(
+ getSettingsAttribute("defaultEngineId"),
+ "",
+ "Should still have an empty settings attribute"
+ );
+
+ // User resets to the original default engine.
+ Services.search.defaultEngine = Services.search.getEngineByName("engine1");
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine1",
+ "Should have the user selected engine as default"
+ );
+ Assert.equal(
+ getSettingsAttribute("defaultEngineId"),
+ "engine1@search.mozilla.orgdefault"
+ );
+
+ // Ending the experiment should keep the original default and reset the
+ // saved attribute.
+ await switchExperiment("");
+
+ Assert.equal(
+ Services.search.appDefaultEngine.name,
+ "engine1",
+ "Should have set the app default engine correctly"
+ );
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine1",
+ "Should have kept the engine the same"
+ );
+ Assert.equal(
+ getSettingsAttribute("defaultEngineId"),
+ "",
+ "Should have reset the saved attribute to empty after the experiment ended"
+ );
+});
+
+add_task(async function test_experiment_setting_user_changed_back_private() {
+ Services.search.defaultPrivateEngine =
+ Services.search.getEngineByName("engine1");
+
+ Assert.equal(
+ Services.search.defaultPrivateEngine.name,
+ "engine1",
+ "Should have the user selected engine as default"
+ );
+ Assert.equal(
+ getSettingsAttribute("privateDefaultEngineId"),
+ "",
+ "Should have an empty settings attribute"
+ );
+
+ // Start the experiment.
+ await switchExperiment("exp2");
+
+ Assert.equal(
+ Services.search.defaultPrivateEngine.name,
+ "exp2",
+ "Should have set the experiment engine as default"
+ );
+ Assert.equal(
+ getSettingsAttribute("defaultEngineId"),
+ "",
+ "Should still have an empty settings attribute"
+ );
+
+ // User resets to the original default engine.
+ Services.search.defaultPrivateEngine =
+ Services.search.getEngineByName("engine1");
+ Assert.equal(
+ Services.search.defaultPrivateEngine.name,
+ "engine1",
+ "Should have the user selected engine as default"
+ );
+ Assert.equal(
+ getSettingsAttribute("privateDefaultEngineId"),
+ "engine1@search.mozilla.orgdefault"
+ );
+
+ // Ending the experiment should keep the original default and reset the
+ // saved attribute.
+ await switchExperiment("");
+
+ Assert.equal(Services.search.appPrivateDefaultEngine.name, "engine1");
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine1",
+ "Should have kept the engine the same "
+ );
+ Assert.equal(
+ getSettingsAttribute("privateDefaultEngineId"),
+ "",
+ "Should have reset the saved attribute to empty after the experiment ended"
+ );
+});
+
+add_task(async function test_experiment_setting_user_changed_to_other_during() {
+ Services.search.defaultEngine = Services.search.getEngineByName("engine1");
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine1",
+ "Should have the application default engine as default"
+ );
+ Assert.equal(
+ getSettingsAttribute("defaultEngineId"),
+ "",
+ "Should have an empty settings attribute"
+ );
+
+ // Start the experiment.
+ await switchExperiment("exp3");
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "exp3",
+ "Should have set the experiment engine as default"
+ );
+ Assert.equal(
+ getSettingsAttribute("defaultEngineId"),
+ "",
+ "Should still have an empty settings attribute"
+ );
+
+ // User changes to a different default engine
+ Services.search.defaultEngine = Services.search.getEngineByName("engine2");
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine2",
+ "Should have the user selected engine as default"
+ );
+ Assert.equal(
+ getSettingsAttribute("defaultEngineId"),
+ "engine2@search.mozilla.orgdefault",
+ "Should have correctly set the user's default in settings"
+ );
+
+ // Ending the experiment should keep the original default and reset the
+ // saved attribute.
+ await switchExperiment("");
+
+ Assert.equal(
+ Services.search.appDefaultEngine.name,
+ "engine1",
+ "Should have set the app default engine correctly"
+ );
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine2",
+ "Should have kept the user's choice of engine"
+ );
+ Assert.equal(
+ getSettingsAttribute("defaultEngineId"),
+ "engine2@search.mozilla.orgdefault",
+ "Should have kept the user's choice in settings"
+ );
+});
+
+add_task(async function test_experiment_setting_user_hid_app_default_during() {
+ // Add all the test engines to be general search engines. This is important
+ // for the test, as the removed experiment engine needs to be a general search
+ // engine, and the first in the list (aided by the orderHint in
+ // data1/engines.json).
+ SearchUtils.GENERAL_SEARCH_ENGINE_IDS.add("engine1@search.mozilla.org");
+ SearchUtils.GENERAL_SEARCH_ENGINE_IDS.add("engine2@search.mozilla.org");
+ SearchUtils.GENERAL_SEARCH_ENGINE_IDS.add("exp2@search.mozilla.org");
+ SearchUtils.GENERAL_SEARCH_ENGINE_IDS.add("exp3@search.mozilla.org");
+
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ false
+ );
+ Services.search.defaultEngine = Services.search.getEngineByName("engine1");
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine1",
+ "Should have the application default engine as default"
+ );
+ Assert.equal(
+ getSettingsAttribute("defaultEngineId"),
+ "",
+ "Should have an empty settings attribute"
+ );
+
+ // Start the experiment.
+ await switchExperiment("exp3");
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "exp3",
+ "Should have set the experiment engine as default"
+ );
+ Assert.equal(
+ getSettingsAttribute("defaultEngineId"),
+ "",
+ "Should still have an empty settings attribute"
+ );
+
+ // User hides the original application engine
+ await Services.search.removeEngine(
+ Services.search.getEngineByName("engine1")
+ );
+ Assert.equal(
+ Services.search.getEngineByName("engine1").hidden,
+ true,
+ "Should have hid the selected engine"
+ );
+
+ // Ending the experiment should keep the original default and reset the
+ // saved attribute.
+ await switchExperiment("");
+
+ Assert.equal(
+ Services.search.appDefaultEngine.name,
+ "engine1",
+ "Should have set the app default engine correctly"
+ );
+ Assert.equal(
+ Services.search.defaultEngine.hidden,
+ false,
+ "Should not have set default engine to an engine that is hidden"
+ );
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine2",
+ "Should have reset the user's engine to the next available engine"
+ );
+ Assert.equal(
+ getSettingsAttribute("defaultEngineId"),
+ "engine2@search.mozilla.orgdefault",
+ "Should have saved the choice in settings"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_defaultEngine_fallback.js b/toolkit/components/search/tests/xpcshell/test_defaultEngine_fallback.js
new file mode 100644
index 0000000000..3d8a36a2e0
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_defaultEngine_fallback.js
@@ -0,0 +1,406 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test is checking the fallbacks when an engine that is default is
+ * removed or hidden.
+ *
+ * The fallback procedure is:
+ *
+ * - Region/Locale default (if visible)
+ * - First visible engine
+ * - If no other visible engines, unhide the region/locale default and use it.
+ */
+
+let appDefault;
+let appPrivateDefault;
+
+add_task(async function setup() {
+ useHttpServer();
+ await SearchTestUtils.useTestEngines();
+
+ Services.prefs.setCharPref(SearchUtils.BROWSER_SEARCH_PREF + "region", "US");
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ true
+ );
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+
+ SearchUtils.GENERAL_SEARCH_ENGINE_IDS = new Set([
+ "engine-resourceicon@search.mozilla.org",
+ "engine-reordered@search.mozilla.org",
+ ]);
+
+ await AddonTestUtils.promiseStartupManager();
+
+ appDefault = await Services.search.getDefault();
+ appPrivateDefault = await Services.search.getDefaultPrivate();
+});
+
+function getDefault(privateMode) {
+ return privateMode
+ ? Services.search.getDefaultPrivate()
+ : Services.search.getDefault();
+}
+
+function setDefault(privateMode, engine) {
+ return privateMode
+ ? Services.search.setDefaultPrivate(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ )
+ : Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+}
+
+async function checkFallbackDefaultRegion(checkPrivate) {
+ let defaultEngine = checkPrivate ? appPrivateDefault : appDefault;
+ let expectedDefaultNotification = checkPrivate
+ ? SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
+ : SearchUtils.MODIFIED_TYPE.DEFAULT;
+ Services.search.restoreDefaultEngines();
+
+ let otherEngine = Services.search.getEngineByName("engine-chromeicon");
+ await setDefault(checkPrivate, otherEngine);
+
+ Assert.notEqual(
+ otherEngine,
+ defaultEngine,
+ "Sanity check engines are different"
+ );
+
+ const observer = new SearchObserver(
+ [
+ expectedDefaultNotification,
+ // For hiding (removing) the engine.
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.MODIFIED_TYPE.REMOVED,
+ ],
+ expectedDefaultNotification
+ );
+
+ await Services.search.removeEngine(otherEngine);
+
+ let notified = await observer.promise;
+
+ Assert.ok(otherEngine.hidden, "Should have hidden the removed engine");
+ Assert.equal(
+ (await getDefault(checkPrivate)).name,
+ defaultEngine.name,
+ "Should have reverted the defaultEngine to the region default"
+ );
+ Assert.equal(
+ notified.name,
+ defaultEngine.name,
+ "Should have notified the correct default engine"
+ );
+}
+
+add_task(async function test_default_fallback_to_region_default() {
+ await checkFallbackDefaultRegion(false);
+});
+
+add_task(async function test_default_private_fallback_to_region_default() {
+ await checkFallbackDefaultRegion(true);
+});
+
+async function checkFallbackFirstVisible(checkPrivate) {
+ let defaultEngine = checkPrivate ? appPrivateDefault : appDefault;
+ let expectedDefaultNotification = checkPrivate
+ ? SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
+ : SearchUtils.MODIFIED_TYPE.DEFAULT;
+ Services.search.restoreDefaultEngines();
+
+ let otherEngine = Services.search.getEngineByName("engine-chromeicon");
+ await setDefault(checkPrivate, otherEngine);
+ await Services.search.removeEngine(defaultEngine);
+
+ Assert.notEqual(
+ otherEngine,
+ defaultEngine,
+ "Sanity check engines are different"
+ );
+
+ const observer = new SearchObserver(
+ checkPrivate
+ ? [
+ expectedDefaultNotification,
+ // For hiding (removing) the engine.
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.MODIFIED_TYPE.REMOVED,
+ ]
+ : [
+ expectedDefaultNotification,
+ // For hiding (removing) the engine.
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.MODIFIED_TYPE.REMOVED,
+ ],
+ expectedDefaultNotification
+ );
+
+ await Services.search.removeEngine(otherEngine);
+
+ let notified = await observer.promise;
+
+ Assert.equal(
+ (await getDefault(checkPrivate)).name,
+ "engine-resourceicon",
+ "Should have set the default engine to the first visible general engine"
+ );
+ Assert.equal(
+ notified.name,
+ "engine-resourceicon",
+ "Should have notified the correct default general engine"
+ );
+}
+
+add_task(async function test_default_fallback_to_first_gen_visible() {
+ await checkFallbackFirstVisible(false);
+});
+
+add_task(async function test_default_private_fallback_to_first_gen_visible() {
+ await checkFallbackFirstVisible(true);
+});
+
+// Removing all visible engines affects both the default and private default
+// engines.
+add_task(async function test_default_fallback_when_no_others_visible() {
+ // Remove all but one of the visible engines.
+ let visibleEngines = await Services.search.getVisibleEngines();
+ for (let i = 0; i < visibleEngines.length - 1; i++) {
+ await Services.search.removeEngine(visibleEngines[i]);
+ }
+ Assert.equal(
+ (await Services.search.getVisibleEngines()).length,
+ 1,
+ "Should only have one visible engine"
+ );
+
+ const observer = new SearchObserver(
+ [
+ // Unhiding of the default engine.
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ // Change of the default.
+ SearchUtils.MODIFIED_TYPE.DEFAULT,
+ // Unhiding of the default private.
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE,
+ // Hiding the engine.
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.MODIFIED_TYPE.REMOVED,
+ ],
+ SearchUtils.MODIFIED_TYPE.DEFAULT
+ );
+
+ // Now remove the last engine, which should set the new default.
+ await Services.search.removeEngine(visibleEngines[visibleEngines.length - 1]);
+
+ let notified = await observer.promise;
+
+ Assert.equal(
+ (await getDefault(false)).name,
+ appDefault.name,
+ "Should fallback to the app default engine after removing all engines"
+ );
+ Assert.equal(
+ (await getDefault(true)).name,
+ appPrivateDefault.name,
+ "Should fallback to the app default private engine after removing all engines"
+ );
+ Assert.equal(
+ notified.name,
+ appDefault.name,
+ "Should have notified the correct default engine"
+ );
+ Assert.ok(
+ !appPrivateDefault.hidden,
+ "Should have unhidden the app default private engine"
+ );
+ Assert.equal(
+ (await Services.search.getVisibleEngines()).length,
+ 2,
+ "Should now have two engines visible"
+ );
+});
+
+add_task(async function test_default_fallback_remove_default_no_visible() {
+ // Remove all but the default engine.
+ Services.search.defaultPrivateEngine = Services.search.defaultEngine;
+ let visibleEngines = await Services.search.getVisibleEngines();
+ for (let engine of visibleEngines) {
+ if (engine.name != appDefault.name) {
+ await Services.search.removeEngine(engine);
+ }
+ }
+ Assert.equal(
+ (await Services.search.getVisibleEngines()).length,
+ 1,
+ "Should only have one visible engine"
+ );
+
+ const observer = new SearchObserver(
+ [
+ // Unhiding of the default engine.
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ // Change of the default.
+ SearchUtils.MODIFIED_TYPE.DEFAULT,
+ SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE,
+ // Hiding the engine.
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.MODIFIED_TYPE.REMOVED,
+ ],
+ SearchUtils.MODIFIED_TYPE.DEFAULT
+ );
+
+ // Now remove the last engine, which should set the new default.
+ await Services.search.removeEngine(appDefault);
+
+ let notified = await observer.promise;
+
+ Assert.equal(
+ (await getDefault(false)).name,
+ "engine-resourceicon",
+ "Should fallback the default engine to the first general search engine"
+ );
+ Assert.equal(
+ (await getDefault(true)).name,
+ "engine-resourceicon",
+ "Should fallback the default private engine to the first general search engine"
+ );
+ Assert.equal(
+ notified.name,
+ "engine-resourceicon",
+ "Should have notified the correct default engine"
+ );
+ Assert.ok(
+ !Services.search.getEngineByName("engine-resourceicon").hidden,
+ "Should have unhidden the new engine"
+ );
+ Assert.equal(
+ (await Services.search.getVisibleEngines()).length,
+ 1,
+ "Should now have one engines visible"
+ );
+});
+
+add_task(
+ async function test_default_fallback_remove_default_no_visible_or_general() {
+ // Reset.
+ Services.search.restoreDefaultEngines();
+ Services.search.defaultEngine = Services.search.defaultPrivateEngine =
+ appPrivateDefault;
+
+ // Remove all but the default engine.
+ let visibleEngines = await Services.search.getVisibleEngines();
+ for (let engine of visibleEngines) {
+ if (engine.name != appPrivateDefault.name) {
+ await Services.search.removeEngine(engine);
+ }
+ }
+ Assert.deepEqual(
+ (await Services.search.getVisibleEngines()).map(e => e.name),
+ appPrivateDefault.name,
+ "Should only have one visible engine"
+ );
+
+ SearchUtils.GENERAL_SEARCH_ENGINE_IDS.clear();
+
+ const observer = new SearchObserver(
+ [
+ // Unhiding of the default engine.
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ // Change of the default.
+ SearchUtils.MODIFIED_TYPE.DEFAULT,
+ SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE,
+ // Hiding the engine.
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.MODIFIED_TYPE.REMOVED,
+ ],
+ SearchUtils.MODIFIED_TYPE.DEFAULT
+ );
+
+ // Now remove the last engine, which should set the new default.
+ await Services.search.removeEngine(appPrivateDefault);
+
+ let notified = await observer.promise;
+
+ Assert.equal(
+ (await getDefault(false)).name,
+ "Test search engine",
+ "Should fallback to the first engine that isn't a general search engine"
+ );
+ Assert.equal(
+ (await getDefault(true)).name,
+ "Test search engine",
+ "Should fallback the private engine to the first engine that isn't a general search engine"
+ );
+ Assert.equal(
+ notified.name,
+ "Test search engine",
+ "Should have notified the correct default engine"
+ );
+ Assert.ok(
+ !Services.search.getEngineByName("Test search engine").hidden,
+ "Should have unhidden the new engine"
+ );
+ Assert.equal(
+ (await Services.search.getVisibleEngines()).length,
+ 1,
+ "Should now have one engines visible"
+ );
+ }
+);
+
+// Test the other remove engine route - for removing non-application provided
+// engines.
+
+async function checkNonBuiltinFallback(checkPrivate) {
+ let defaultEngine = checkPrivate ? appPrivateDefault : appDefault;
+ let expectedDefaultNotification = checkPrivate
+ ? SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
+ : SearchUtils.MODIFIED_TYPE.DEFAULT;
+ Services.search.restoreDefaultEngines();
+
+ let addedEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engine2.xml`,
+ });
+
+ await setDefault(checkPrivate, addedEngine);
+
+ const observer = new SearchObserver(
+ [expectedDefaultNotification, SearchUtils.MODIFIED_TYPE.REMOVED],
+ expectedDefaultNotification
+ );
+
+ // Remove the current engine...
+ await Services.search.removeEngine(addedEngine);
+
+ // ... and verify we've reverted to the normal default engine.
+ Assert.equal(
+ (await getDefault(checkPrivate)).name,
+ defaultEngine.name,
+ "Should revert to the app default engine"
+ );
+
+ let notified = await observer.promise;
+ Assert.equal(
+ notified.name,
+ defaultEngine.name,
+ "Should have notified the correct default engine"
+ );
+}
+
+add_task(async function test_default_fallback_non_builtin() {
+ await checkNonBuiltinFallback(false);
+});
+
+add_task(async function test_default_fallback_non_builtin_private() {
+ await checkNonBuiltinFallback(true);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_defaultPrivateEngine.js b/toolkit/components/search/tests/xpcshell/test_defaultPrivateEngine.js
new file mode 100644
index 0000000000..74c0dc5188
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_defaultPrivateEngine.js
@@ -0,0 +1,582 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test that defaultEngine property can be set and yields the proper events and\
+ * behavior (search results)
+ */
+
+"use strict";
+
+let engine1;
+let engine2;
+let appDefault;
+let appPrivateDefault;
+
+add_setup(async () => {
+ do_get_profile();
+ Services.fog.initializeFOG();
+
+ await SearchTestUtils.useTestEngines();
+
+ Services.prefs.setCharPref(SearchUtils.BROWSER_SEARCH_PREF + "region", "US");
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ true
+ );
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+
+ useHttpServer("opensearch");
+ await AddonTestUtils.promiseStartupManager();
+
+ await Services.search.init();
+
+ appDefault = Services.search.appDefaultEngine;
+ appPrivateDefault = Services.search.appPrivateDefaultEngine;
+ engine1 = Services.search.getEngineByName("engine-rel-searchform-purpose");
+ engine2 = Services.search.getEngineByName("engine-chromeicon");
+});
+
+add_task(async function test_defaultPrivateEngine() {
+ Assert.equal(
+ Services.search.defaultPrivateEngine,
+ appPrivateDefault,
+ "Should have the app private default as the default private engine"
+ );
+ Assert.equal(
+ Services.search.defaultEngine,
+ appDefault,
+ "Should have the app default as the default engine"
+ );
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine",
+ displayName: "Test search engine",
+ loadPath: "[addon]engine@search.mozilla.org",
+ submissionUrl: "https://www.google.com/search?q=",
+ verified: "default",
+ },
+ private: {
+ engineId: "engine-pref",
+ displayName: "engine-pref",
+ loadPath: "[addon]engine-pref@search.mozilla.org",
+ submissionUrl: "https://www.google.com/search?q=",
+ verified: "default",
+ },
+ });
+
+ let promise = promiseDefaultNotification("private");
+ Services.search.defaultPrivateEngine = engine1;
+ Assert.equal(
+ await promise,
+ engine1,
+ "Should have notified setting the private engine to the new one"
+ );
+
+ Assert.equal(
+ Services.search.defaultPrivateEngine,
+ engine1,
+ "Should have set the private engine to the new one"
+ );
+ Assert.equal(
+ Services.search.defaultEngine,
+ appDefault,
+ "Should not have changed the default engine"
+ );
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine",
+ displayName: "Test search engine",
+ loadPath: "[addon]engine@search.mozilla.org",
+ submissionUrl: "https://www.google.com/search?q=",
+ verified: "default",
+ },
+ private: {
+ engineId: "engine-rel-searchform-purpose",
+ displayName: "engine-rel-searchform-purpose",
+ loadPath: "[addon]engine-rel-searchform-purpose@search.mozilla.org",
+ submissionUrl: "https://www.google.com/search?q=&channel=sb",
+ verified: "default",
+ },
+ });
+
+ promise = promiseDefaultNotification("private");
+ await Services.search.setDefaultPrivate(
+ engine2,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ Assert.equal(
+ await promise,
+ engine2,
+ "Should have notified setting the private engine to the new one using async api"
+ );
+ Assert.equal(
+ Services.search.defaultPrivateEngine,
+ engine2,
+ "Should have set the private engine to the new one using the async api"
+ );
+
+ // We use the names here as for some reason the getDefaultPrivate promise
+ // returns something which is an nsISearchEngine but doesn't compare
+ // exactly to what engine2 is.
+ Assert.equal(
+ (await Services.search.getDefaultPrivate()).name,
+ engine2.name,
+ "Should have got the correct private engine with the async api"
+ );
+ Assert.equal(
+ Services.search.defaultEngine,
+ appDefault,
+ "Should not have changed the default engine"
+ );
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine",
+ displayName: "Test search engine",
+ loadPath: "[addon]engine@search.mozilla.org",
+ submissionUrl: "https://www.google.com/search?q=",
+ verified: "default",
+ },
+ private: {
+ engineId: "engine-chromeicon",
+ displayName: "engine-chromeicon",
+ loadPath: "[addon]engine-chromeicon@search.mozilla.org",
+ submissionUrl: "https://www.google.com/search?q=",
+ verified: "default",
+ },
+ });
+
+ promise = promiseDefaultNotification("private");
+ await Services.search.setDefaultPrivate(
+ engine1,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ Assert.equal(
+ await promise,
+ engine1,
+ "Should have notified reverting the private engine to the selected one using async api"
+ );
+ Assert.equal(
+ Services.search.defaultPrivateEngine,
+ engine1,
+ "Should have reverted the private engine to the selected one using the async api"
+ );
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine",
+ },
+ private: {
+ engineId: "engine-rel-searchform-purpose",
+ },
+ });
+
+ engine1.hidden = true;
+ Assert.equal(
+ Services.search.defaultPrivateEngine,
+ appPrivateDefault,
+ "Should reset to the app default private engine when hiding the default"
+ );
+ Assert.equal(
+ Services.search.defaultEngine,
+ appDefault,
+ "Should not have changed the default engine"
+ );
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine",
+ },
+ private: {
+ engineId: "engine-pref",
+ },
+ });
+
+ engine1.hidden = false;
+ Services.search.defaultEngine = engine1;
+ Assert.equal(
+ Services.search.defaultPrivateEngine,
+ appPrivateDefault,
+ "Setting the default engine should not affect the private default"
+ );
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine-rel-searchform-purpose",
+ },
+ private: {
+ engineId: "engine-pref",
+ },
+ });
+
+ Services.search.defaultEngine = appDefault;
+});
+
+add_task(async function test_telemetry_private_empty_submission_url() {
+ let engine = await Services.search.addOpenSearchEngine(
+ gDataUrl + "simple.xml",
+ null
+ );
+ Services.search.defaultPrivateEngine = engine;
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: appDefault.telemetryId,
+ },
+ private: {
+ engineId: "other-simple",
+ displayName: "simple",
+ loadPath: "[http]localhost/simple.xml",
+ submissionUrl: "blank:",
+ verified: "verified",
+ },
+ });
+
+ Services.search.defaultEngine = appDefault;
+});
+
+add_task(async function test_defaultPrivateEngine_turned_off() {
+ Services.search.defaultEngine = appDefault;
+ Services.search.defaultPrivateEngine = engine1;
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine",
+ },
+ private: {
+ engineId: "engine-rel-searchform-purpose",
+ },
+ });
+
+ let promise = promiseDefaultNotification("private");
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ false
+ );
+ Assert.equal(
+ await promise,
+ appDefault,
+ "Should have notified setting the first engine correctly."
+ );
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine",
+ },
+ private: {
+ engineId: "",
+ },
+ });
+
+ promise = promiseDefaultNotification("normal");
+ let privatePromise = promiseDefaultNotification("private");
+ Services.search.defaultEngine = engine1;
+ Assert.equal(
+ await promise,
+ engine1,
+ "Should have notified setting the first engine correctly."
+ );
+ Assert.equal(
+ await privatePromise,
+ engine1,
+ "Should have notified setting of the private engine as well."
+ );
+ Assert.equal(
+ Services.search.defaultPrivateEngine,
+ engine1,
+ "Should be set to the first engine correctly"
+ );
+ Assert.equal(
+ Services.search.defaultEngine,
+ engine1,
+ "Should keep the default engine in sync with the pref off"
+ );
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine-rel-searchform-purpose",
+ },
+ private: {
+ engineId: "",
+ },
+ });
+
+ promise = promiseDefaultNotification("private");
+ Services.search.defaultPrivateEngine = engine2;
+ Assert.equal(
+ await promise,
+ engine2,
+ "Should have notified setting the second engine correctly."
+ );
+ Assert.equal(
+ Services.search.defaultPrivateEngine,
+ engine2,
+ "Should be set to the second engine correctly"
+ );
+ Assert.equal(
+ Services.search.defaultEngine,
+ engine1,
+ "Should not change the normal mode default engine"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ false
+ ),
+ true,
+ "Should have set the separate private default pref to true"
+ );
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine-rel-searchform-purpose",
+ },
+ private: {
+ engineId: "engine-chromeicon",
+ },
+ });
+
+ promise = promiseDefaultNotification("private");
+ Services.search.defaultPrivateEngine = engine1;
+ Assert.equal(
+ await promise,
+ engine1,
+ "Should have notified resetting to the first engine again"
+ );
+ Assert.equal(
+ Services.search.defaultPrivateEngine,
+ engine1,
+ "Should be reset to the first engine again"
+ );
+ Assert.equal(
+ Services.search.defaultEngine,
+ engine1,
+ "Should keep the default engine in sync with the pref off"
+ );
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine-rel-searchform-purpose",
+ },
+ private: {
+ engineId: "engine-rel-searchform-purpose",
+ },
+ });
+});
+
+add_task(async function test_defaultPrivateEngine_ui_turned_off() {
+ engine1.hidden = false;
+ engine2.hidden = false;
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+
+ Services.search.defaultEngine = engine2;
+ Services.search.defaultPrivateEngine = engine1;
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine-chromeicon",
+ },
+ private: {
+ engineId: "engine-rel-searchform-purpose",
+ },
+ });
+
+ let promise = promiseDefaultNotification("private");
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ false
+ );
+ Assert.equal(
+ await promise,
+ engine2,
+ "Should have notified for resetting of the private pref."
+ );
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine-chromeicon",
+ },
+ private: {
+ engineId: "",
+ },
+ });
+
+ promise = promiseDefaultNotification("normal");
+ Services.search.defaultPrivateEngine = engine1;
+ Assert.equal(
+ await promise,
+ engine1,
+ "Should have notified setting the first engine correctly."
+ );
+ Assert.equal(
+ Services.search.defaultPrivateEngine,
+ engine1,
+ "Should be set to the first engine correctly"
+ );
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine-rel-searchform-purpose",
+ },
+ private: {
+ engineId: "",
+ },
+ });
+});
+
+add_task(async function test_defaultPrivateEngine_same_engine_toggle_pref() {
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ true
+ );
+
+ // Set the normal and private engines to be the same
+ Services.search.defaultEngine = engine2;
+ Services.search.defaultPrivateEngine = engine2;
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine-chromeicon",
+ },
+ private: {
+ engineId: "engine-chromeicon",
+ },
+ });
+
+ // Disable pref
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ false
+ );
+ Assert.equal(
+ Services.search.defaultPrivateEngine,
+ engine2,
+ "Should not change the default private engine"
+ );
+ Assert.equal(
+ Services.search.defaultEngine,
+ engine2,
+ "Should not change the default engine"
+ );
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine-chromeicon",
+ },
+ private: {
+ engineId: "",
+ },
+ });
+
+ // Re-enable pref
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+ Assert.equal(
+ Services.search.defaultPrivateEngine,
+ engine2,
+ "Should not change the default private engine"
+ );
+ Assert.equal(
+ Services.search.defaultEngine,
+ engine2,
+ "Should not change the default engine"
+ );
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine-chromeicon",
+ },
+ private: {
+ engineId: "engine-chromeicon",
+ },
+ });
+});
+
+add_task(async function test_defaultPrivateEngine_same_engine_toggle_ui_pref() {
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ true
+ );
+
+ // Set the normal and private engines to be the same
+ Services.search.defaultEngine = engine2;
+ Services.search.defaultPrivateEngine = engine2;
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine-chromeicon",
+ },
+ private: {
+ engineId: "engine-chromeicon",
+ },
+ });
+
+ // Disable UI pref
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ false
+ );
+ Assert.equal(
+ Services.search.defaultPrivateEngine,
+ engine2,
+ "Should not change the default private engine"
+ );
+ Assert.equal(
+ Services.search.defaultEngine,
+ engine2,
+ "Should not change the default engine"
+ );
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine-chromeicon",
+ },
+ private: {
+ engineId: "",
+ },
+ });
+
+ // Re-enable UI pref
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ true
+ );
+ Assert.equal(
+ Services.search.defaultPrivateEngine,
+ engine2,
+ "Should not change the default private engine"
+ );
+ Assert.equal(
+ Services.search.defaultEngine,
+ engine2,
+ "Should not change the default engine"
+ );
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine-chromeicon",
+ },
+ private: {
+ engineId: "engine-chromeicon",
+ },
+ });
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_engine_alias.js b/toolkit/components/search/tests/xpcshell/test_engine_alias.js
new file mode 100644
index 0000000000..62c3c141c1
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_engine_alias.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const NAME = "Test Alias Engine";
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+ let settingsFileWritten = promiseAfterSettings();
+ await Services.search.init();
+ await settingsFileWritten;
+});
+
+add_task(async function upgrade_with_configuration_change_test() {
+ let settingsFileWritten = promiseAfterSettings();
+ await SearchTestUtils.installSearchExtension({
+ name: NAME,
+ keyword: "testalias",
+ });
+ await settingsFileWritten;
+
+ let engine = await Services.search.getEngineByAlias("testalias");
+ Assert.equal(engine?.name, NAME, "Engine can be fetched by alias");
+
+ // Restart the search service but not the AddonManager, we will
+ // load the engines from settings.
+ Services.search.wrappedJSObject.reset();
+ await Services.search.init();
+
+ engine = await Services.search.getEngineByAlias("testalias");
+ Assert.equal(engine?.name, NAME, "Engine can be fetched by alias");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_engine_ids.js b/toolkit/components/search/tests/xpcshell/test_engine_ids.js
new file mode 100644
index 0000000000..c6e5e7e148
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_engine_ids.js
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests Search Engine IDs are created correctly.
+ */
+
+"use strict";
+
+const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+);
+
+const CONFIG = [
+ {
+ webExtension: {
+ id: "engine@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ default: "yes",
+ },
+ ],
+ },
+];
+
+add_task(async function setup() {
+ useHttpServer("opensearch");
+ await SearchTestUtils.useTestEngines("data", null, CONFIG);
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+});
+
+add_task(async function test_add_on_engine_id() {
+ let addOnEngine = Services.search.defaultEngine;
+
+ Assert.equal(
+ addOnEngine.name,
+ "Test search engine",
+ "Should have installed the Test search engine as default."
+ );
+ Assert.ok(addOnEngine.id, "The Addon Search Engine should have an id.");
+ Assert.equal(
+ addOnEngine.id,
+ "engine@search.mozilla.orgdefault",
+ "The Addon Search Engine id should be the webextension id + the locale."
+ );
+});
+
+add_task(async function test_user_engine_id() {
+ let promiseEngineAdded = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.ADDED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+
+ await Services.search.addUserEngine(
+ "user",
+ "https://example.com/user?q={searchTerms}",
+ "u"
+ );
+
+ await promiseEngineAdded;
+ let userEngine = Services.search.getEngineByName("user");
+
+ Assert.ok(userEngine, "Should have installed the User Search Engine.");
+ Assert.ok(userEngine.id, "The User Search Engine should have an id.");
+ Assert.equal(
+ userEngine.id.length,
+ 36,
+ "The User Search Engine id should be a 36 character uuid."
+ );
+});
+
+add_task(async function test_open_search_engine_id() {
+ let promiseEngineAdded = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.ADDED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+
+ let openSearchEngine = await Services.search.addOpenSearchEngine(
+ gDataUrl + "simple.xml",
+ null
+ );
+
+ await promiseEngineAdded;
+
+ Assert.ok(openSearchEngine, "Should have installed the Open Search Engine.");
+ Assert.ok(openSearchEngine.id, "The Open Search Engine should have an id.");
+ Assert.equal(
+ openSearchEngine.id.length,
+ 36,
+ "The Open Search Engine id should be a 36 character uuid."
+ );
+});
+
+add_task(async function test_enterprise_policy_engine_id() {
+ await setupPolicyEngineWithJson({
+ policies: {
+ SearchEngines: {
+ Add: [
+ {
+ Name: "policy",
+ Description: "Test policy engine",
+ IconURL: "data:image/gif;base64,R0lGODl",
+ Alias: "p",
+ URLTemplate: "https://example.com?q={searchTerms}",
+ SuggestURLTemplate: "https://example.com/suggest/?q={searchTerms}",
+ },
+ ],
+ },
+ },
+ });
+
+ let policyEngine = Services.search.getEngineByName("policy");
+
+ Assert.ok(policyEngine, "Should have installed the Policy Engine.");
+ Assert.ok(policyEngine.id, "The Policy Engine should have an id.");
+ Assert.equal(
+ policyEngine.id,
+ "policy-policy",
+ "The Policy Engine id should be 'policy-' + 'the name of the policy engine'."
+ );
+});
+
+/**
+ * Loads a new enterprise policy, and re-initialise the search service
+ * with the new policy. Also waits for the search service to write the settings
+ * file to disk.
+ *
+ * @param {object} policy
+ * The enterprise policy to use.
+ */
+async function setupPolicyEngineWithJson(policy) {
+ Services.search.wrappedJSObject.reset();
+
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson(policy);
+
+ let settingsWritten = SearchTestUtils.promiseSearchNotification(
+ "write-settings-to-disk-complete"
+ );
+ await Services.search.init();
+ await settingsWritten;
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_engine_multiple_alias.js b/toolkit/components/search/tests/xpcshell/test_engine_multiple_alias.js
new file mode 100644
index 0000000000..f4a7905d45
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_engine_multiple_alias.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const NAME = "Test Alias Engine";
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+});
+
+add_task(async function upgrade_with_configuration_change_test() {
+ let settingsFileWritten = promiseAfterSettings();
+ await SearchTestUtils.installSearchExtension({
+ name: NAME,
+ keyword: [" test", "alias "],
+ });
+ await settingsFileWritten;
+
+ let engine = await Services.search.getEngineByAlias("test");
+ Assert.equal(engine?.name, NAME, "Can be fetched by either alias");
+ engine = await Services.search.getEngineByAlias("alias");
+ Assert.equal(engine?.name, NAME, "Can be fetched by either alias");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_engine_selector.js b/toolkit/components/search/tests/xpcshell/test_engine_selector.js
new file mode 100644
index 0000000000..7052a7de76
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_engine_selector.js
@@ -0,0 +1,241 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs",
+});
+
+const TEST_CONFIG = [
+ {
+ engineName: "aol",
+ orderHint: 500,
+ webExtension: {
+ locales: ["default"],
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ {
+ included: { regions: ["us"] },
+ webExtension: {
+ locales: ["baz-$USER_LOCALE"],
+ },
+ telemetryId: "foo-$USER_LOCALE",
+ },
+ {
+ included: { regions: ["fr"] },
+ webExtension: {
+ locales: ["region-$USER_REGION"],
+ },
+ telemetryId: "bar-$USER_REGION",
+ },
+ {
+ included: { regions: ["be"] },
+ webExtension: {
+ locales: ["$USER_LOCALE"],
+ },
+ telemetryId: "$USER_LOCALE",
+ },
+ {
+ included: { regions: ["au"] },
+ webExtension: {
+ locales: ["$USER_REGION"],
+ },
+ telemetryId: "$USER_REGION",
+ },
+ ],
+ },
+ {
+ engineName: "lycos",
+ orderHint: 1000,
+ default: "yes",
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ excluded: { locales: { matches: ["zh-CN"] } },
+ },
+ ],
+ },
+ {
+ engineName: "altavista",
+ orderHint: 2000,
+ defaultPrivate: "yes",
+ appliesTo: [
+ {
+ included: { locales: { matches: ["en-US"] } },
+ },
+ {
+ included: { regions: ["default"] },
+ },
+ ],
+ },
+ {
+ engineName: "excite",
+ default: "yes-if-no-other",
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ excluded: { regions: ["us"] },
+ },
+ {
+ included: { everywhere: true },
+ experiment: "acohortid",
+ },
+ ],
+ },
+ {
+ engineName: "askjeeves",
+ },
+];
+
+const engineSelector = new SearchEngineSelector();
+
+add_task(async function setup() {
+ const settings = await RemoteSettings(SearchUtils.SETTINGS_KEY);
+ sinon.stub(settings, "get").returns(TEST_CONFIG);
+});
+
+add_task(async function test_engine_selector() {
+ let { engines, privateDefault } =
+ await engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "us",
+ });
+ Assert.equal(
+ privateDefault.engineName,
+ "altavista",
+ "Should set altavista as privateDefault"
+ );
+ let names = engines.map(obj => obj.engineName);
+ Assert.deepEqual(names, ["lycos", "altavista", "aol"], "Correct order");
+ Assert.equal(
+ engines[2].webExtension.locale,
+ "baz-en-US",
+ "Subsequent matches in applies to can override default"
+ );
+
+ ({ engines, privateDefault } = await engineSelector.fetchEngineConfiguration({
+ locale: "zh-CN",
+ region: "kz",
+ }));
+ Assert.equal(engines.length, 2, "Correct engines are returns");
+ Assert.equal(privateDefault, null, "There should be no privateDefault");
+ names = engines.map(obj => obj.engineName);
+ Assert.deepEqual(
+ names,
+ ["excite", "aol"],
+ "The engines should be in the correct order"
+ );
+
+ ({ engines, privateDefault } = await engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "us",
+ experiment: "acohortid",
+ }));
+ Assert.deepEqual(
+ engines.map(obj => obj.engineName),
+ ["lycos", "altavista", "aol", "excite"],
+ "Engines are in the correct order and include the experiment engine"
+ );
+
+ ({ engines, privateDefault } = await engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "default",
+ experiment: "acohortid",
+ }));
+ Assert.deepEqual(
+ engines.map(obj => obj.engineName),
+ ["lycos", "altavista", "aol", "excite"],
+ "The engines should be in the correct order"
+ );
+ Assert.equal(
+ privateDefault.engineName,
+ "altavista",
+ "Should set altavista as privateDefault"
+ );
+});
+
+add_task(async function test_locale_region_replacement() {
+ let { engines } = await engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "us",
+ });
+ let engine = engines.find(e => e.engineName == "aol");
+ Assert.equal(
+ engine.webExtension.locale,
+ "baz-en-US",
+ "The locale is correctly inserted into the locale field"
+ );
+ Assert.equal(
+ engine.telemetryId,
+ "foo-en-US",
+ "The locale is correctly inserted into the telemetryId"
+ );
+
+ ({ engines } = await engineSelector.fetchEngineConfiguration({
+ locale: "it",
+ region: "us",
+ }));
+ engine = engines.find(e => e.engineName == "aol");
+
+ Assert.equal(
+ engines.find(e => e.engineName == "aol").webExtension.locale,
+ "baz-it",
+ "The locale is correctly inserted into the locale field"
+ );
+ Assert.equal(
+ engine.telemetryId,
+ "foo-it",
+ "The locale is correctly inserted into the telemetryId"
+ );
+
+ ({ engines } = await engineSelector.fetchEngineConfiguration({
+ locale: "en-CA",
+ region: "fr",
+ }));
+ engine = engines.find(e => e.engineName == "aol");
+ Assert.equal(
+ engine.webExtension.locale,
+ "region-fr",
+ "The region is correctly inserted into the locale field"
+ );
+ Assert.equal(
+ engine.telemetryId,
+ "bar-fr",
+ "The region is correctly inserted into the telemetryId"
+ );
+
+ ({ engines } = await engineSelector.fetchEngineConfiguration({
+ locale: "fy-NL",
+ region: "be",
+ }));
+ engine = engines.find(e => e.engineName == "aol");
+ Assert.equal(
+ engine.webExtension.locale,
+ "fy-NL",
+ "The locale is correctly inserted into the locale field"
+ );
+ Assert.equal(
+ engine.telemetryId,
+ "fy-NL",
+ "The locale is correctly inserted into the telemetryId"
+ );
+ ({ engines } = await engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "au",
+ }));
+ engine = engines.find(e => e.engineName == "aol");
+ Assert.equal(
+ engine.webExtension.locale,
+ "au",
+ "The region is correctly inserted into the locale field"
+ );
+ Assert.equal(
+ engine.telemetryId,
+ "au",
+ "The region is correctly inserted into the telemetryId"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_engine_selector_application.js b/toolkit/components/search/tests/xpcshell/test_engine_selector_application.js
new file mode 100644
index 0000000000..ef54b763af
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_engine_selector_application.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs",
+});
+
+const TEST_CONFIG = [
+ {
+ webExtension: {
+ id: "aol@example.com",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ ],
+ default: "yes-if-no-other",
+ },
+ {
+ webExtension: {
+ id: "lycos@example.com",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ application: {
+ channel: ["nightly"],
+ },
+ },
+ ],
+ default: "yes",
+ },
+ {
+ webExtension: {
+ id: "altavista@example.com",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ application: {
+ channel: ["nightly", "esr"],
+ },
+ },
+ ],
+ },
+ {
+ webExtension: {
+ id: "excite@example.com",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ {
+ included: { everywhere: true },
+ application: {
+ channel: ["release"],
+ },
+ default: "yes",
+ },
+ ],
+ },
+];
+
+const expectedEnginesPerChannel = {
+ default: ["aol@example.com", "excite@example.com"],
+ nightly: [
+ "lycos@example.com",
+ "aol@example.com",
+ "altavista@example.com",
+ "excite@example.com",
+ ],
+ beta: ["aol@example.com", "excite@example.com"],
+ release: ["excite@example.com", "aol@example.com"],
+ esr: ["aol@example.com", "altavista@example.com", "excite@example.com"],
+};
+
+const expectedDefaultEngine = {
+ default: "aol@example.com",
+ nightly: "lycos@example.com",
+ beta: "aol@example.com",
+ release: "excite@example.com",
+ esr: "aol@example.com",
+};
+
+const engineSelector = new SearchEngineSelector();
+
+add_task(async function test_engine_selector_channels() {
+ const settings = await RemoteSettings(SearchUtils.SETTINGS_KEY);
+ sinon.stub(settings, "get").returns(TEST_CONFIG);
+
+ for (let [channel, expected] of Object.entries(expectedEnginesPerChannel)) {
+ const { engines } = await engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "us",
+ channel,
+ });
+
+ const engineIds = engines.map(obj => obj.webExtension.id);
+ Assert.deepEqual(
+ engineIds,
+ expected,
+ `Should have the expected engines for channel "${channel}"`
+ );
+
+ Assert.equal(
+ engineIds[0],
+ expectedDefaultEngine[channel],
+ `Should have the correct default for channel "${channel}"`
+ );
+ }
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_engine_selector_application_distribution.js b/toolkit/components/search/tests/xpcshell/test_engine_selector_application_distribution.js
new file mode 100644
index 0000000000..2e29eac198
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_engine_selector_application_distribution.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs",
+});
+
+const CONFIG = [
+ {
+ webExtension: {
+ id: "aol@example.com",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ ],
+ default: "yes-if-no-other",
+ },
+ {
+ webExtension: {
+ id: "excite@example.com",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ // Test with a application/distributions section present but an
+ // empty list.
+ application: {
+ distributions: [],
+ },
+ },
+ ],
+ },
+ {
+ webExtension: {
+ id: "lycos@example.com",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ application: {
+ distributions: ["cake"],
+ },
+ },
+ ],
+ default: "yes",
+ },
+ {
+ webExtension: {
+ id: "altavista@example.com",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ application: {
+ excludedDistributions: ["apples"],
+ },
+ },
+ ],
+ },
+];
+
+const engineSelector = new SearchEngineSelector();
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("data", null, CONFIG);
+ await AddonTestUtils.promiseStartupManager();
+});
+
+add_task(async function test_no_distribution_preference() {
+ let { engines } = await engineSelector.fetchEngineConfiguration({
+ locale: "default",
+ region: "default",
+ channel: "",
+ distroID: "",
+ });
+ const engineIds = engines.map(obj => obj.webExtension.id);
+ Assert.deepEqual(
+ engineIds,
+ ["aol@example.com", "excite@example.com", "altavista@example.com"],
+ `Should have the expected engines for a normal build.`
+ );
+});
+
+add_task(async function test_distribution_included() {
+ let { engines } = await engineSelector.fetchEngineConfiguration({
+ locale: "default",
+ region: "default",
+ channel: "",
+ distroID: "cake",
+ });
+ const engineIds = engines.map(obj => obj.webExtension.id);
+ Assert.deepEqual(
+ engineIds,
+ [
+ "lycos@example.com",
+ "aol@example.com",
+ "excite@example.com",
+ "altavista@example.com",
+ ],
+ `Should have the expected engines for the "cake" distribution.`
+ );
+});
+
+add_task(async function test_distribution_excluded() {
+ let { engines } = await engineSelector.fetchEngineConfiguration({
+ locale: "default",
+ region: "default",
+ channel: "",
+ distroID: "apples",
+ });
+ const engineIds = engines.map(obj => obj.webExtension.id);
+ Assert.deepEqual(
+ engineIds,
+ ["aol@example.com", "excite@example.com"],
+ `Should have the expected engines for the "apples" distribution.`
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_engine_selector_application_name.js b/toolkit/components/search/tests/xpcshell/test_engine_selector_application_name.js
new file mode 100644
index 0000000000..fef03c47be
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_engine_selector_application_name.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs",
+});
+
+const CONFIG = [
+ {
+ webExtension: {
+ id: "aol@example.com",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ ],
+ default: "yes-if-no-other",
+ },
+ {
+ webExtension: {
+ id: "lycos@example.com",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ application: {
+ name: ["firefox"],
+ },
+ },
+ ],
+ default: "yes",
+ },
+ {
+ webExtension: {
+ id: "altavista@example.com",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ application: {
+ name: ["fenix"],
+ },
+ },
+ ],
+ },
+ {
+ webExtension: {
+ id: "excite@example.com",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ application: {
+ name: ["firefox"],
+ minVersion: "10",
+ maxVersion: "30",
+ },
+ default: "yes",
+ },
+ ],
+ },
+];
+
+const engineSelector = new SearchEngineSelector();
+
+const tests = [
+ {
+ name: "Firefox",
+ version: "1",
+ expected: ["lycos@example.com", "aol@example.com"],
+ },
+ {
+ name: "Firefox",
+ version: "20",
+ expected: ["lycos@example.com", "aol@example.com", "excite@example.com"],
+ },
+ {
+ name: "Fenix",
+ version: "20",
+ expected: ["aol@example.com", "altavista@example.com"],
+ },
+ {
+ name: "Firefox",
+ version: "31",
+ expected: ["lycos@example.com", "aol@example.com"],
+ },
+ {
+ name: "Firefox",
+ version: "30",
+ expected: ["lycos@example.com", "aol@example.com", "excite@example.com"],
+ },
+ {
+ name: "Firefox",
+ version: "10",
+ expected: ["lycos@example.com", "aol@example.com", "excite@example.com"],
+ },
+];
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("data", null, CONFIG);
+ await AddonTestUtils.promiseStartupManager();
+
+ let confUrl = `data:application/json,${JSON.stringify(CONFIG)}`;
+ Services.prefs.setStringPref("search.config.url", confUrl);
+});
+
+add_task(async function test_application_name() {
+ for (const { name, version, expected } of tests) {
+ let { engines } = await engineSelector.fetchEngineConfiguration({
+ locale: "default",
+ region: "default",
+ name,
+ version,
+ });
+ const engineIds = engines.map(obj => obj.webExtension.id);
+ Assert.deepEqual(
+ engineIds,
+ expected,
+ `Should have the expected engines for app: "${name}"
+ and version: "${version}"`
+ );
+ }
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_engine_selector_order.js b/toolkit/components/search/tests/xpcshell/test_engine_selector_order.js
new file mode 100644
index 0000000000..1de4792af1
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_engine_selector_order.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs",
+});
+
+/**
+ * This constant defines the tests for the order. The input is an array of
+ * engines that will be constructed. The engine definitions are arrays with
+ * fields in order:
+ * name, orderHint, default, defaultPrivate
+ *
+ * The expected is an array of engine names.
+ */
+const TESTS = [
+ {
+ // Basic tests to ensure correct order for default engine.
+ input: [
+ ["A", 750, "no", "no"],
+ ["B", 3000, "no", "no"],
+ ["C", 2000, "yes", "no"],
+ ["D", 1000, "yes-if-no-other", "no"],
+ ["E", 500, "no", "no"],
+ ],
+ expected: ["C", "B", "D", "A", "E"],
+ expectedPrivate: undefined,
+ },
+ {
+ input: [
+ ["A", 750, "no", "no"],
+ ["B", 3000, "no", "no"],
+ ["C", 2000, "yes-if-no-other", "no"],
+ ["D", 1000, "yes", "no"],
+ ["E", 500, "no", "no"],
+ ],
+ expected: ["D", "B", "C", "A", "E"],
+ expectedPrivate: undefined,
+ },
+ // Check that yes-if-no-other works correctly.
+ {
+ input: [
+ ["A", 750, "no", "no"],
+ ["B", 3000, "no", "no"],
+ ["C", 2000, "no", "no"],
+ ["D", 1000, "yes-if-no-other", "no"],
+ ["E", 500, "no", "no"],
+ ],
+ expected: ["D", "B", "C", "A", "E"],
+ expectedPrivate: undefined,
+ },
+ // Basic tests to ensure correct order with private engine.
+ {
+ input: [
+ ["A", 750, "no", "no"],
+ ["B", 3000, "yes-if-no-other", "no"],
+ ["C", 2000, "no", "yes"],
+ ["D", 1000, "yes", "yes-if-no-other"],
+ ["E", 500, "no", "no"],
+ ],
+ expected: ["D", "C", "B", "A", "E"],
+ expectedPrivate: "C",
+ },
+ {
+ input: [
+ ["A", 750, "no", "yes-if-no-other"],
+ ["B", 3000, "yes-if-no-other", "no"],
+ ["C", 2000, "no", "yes"],
+ ["D", 1000, "yes", "no"],
+ ["E", 500, "no", "no"],
+ ],
+ expected: ["D", "C", "B", "A", "E"],
+ expectedPrivate: "C",
+ },
+ // Private engine test for yes-if-no-other.
+ {
+ input: [
+ ["A", 750, "no", "yes-if-no-other"],
+ ["B", 3000, "yes-if-no-other", "no"],
+ ["C", 2000, "no", "no"],
+ ["D", 1000, "yes", "no"],
+ ["E", 500, "no", "no"],
+ ],
+ expected: ["D", "A", "B", "C", "E"],
+ expectedPrivate: "A",
+ },
+];
+
+function getConfigData(testInput) {
+ return testInput.map(info => ({
+ engineName: info[0],
+ orderHint: info[1],
+ default: info[2],
+ defaultPrivate: info[3],
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ ],
+ }));
+}
+
+const engineSelector = new SearchEngineSelector();
+
+add_task(async function () {
+ const settings = await RemoteSettings(SearchUtils.SETTINGS_KEY);
+ const getStub = sinon.stub(settings, "get");
+
+ let i = 0;
+ for (const test of TESTS) {
+ // Remove the existing configuration and update the stub to return the data
+ // for this test.
+ delete engineSelector._configuration;
+ getStub.returns(getConfigData(test.input));
+
+ const { engines, privateDefault } =
+ await engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "us",
+ });
+
+ let names = engines.map(obj => obj.engineName);
+ Assert.deepEqual(
+ names,
+ test.expected,
+ `Should have the correct order for the engines: test ${i}`
+ );
+ Assert.equal(
+ privateDefault && privateDefault.engineName,
+ test.expectedPrivate,
+ `Should have the correct selection for the private engine: test ${i++}`
+ );
+ }
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_engine_selector_override.js b/toolkit/components/search/tests/xpcshell/test_engine_selector_override.js
new file mode 100644
index 0000000000..e9aa1c073f
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_engine_selector_override.js
@@ -0,0 +1,181 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs",
+});
+
+const TEST_CONFIG = [
+ {
+ webExtension: {
+ id: "aol@example.com",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ {
+ override: true,
+ application: {
+ distributions: ["distro1"],
+ },
+ params: {
+ searchUrlGetParams: [
+ {
+ name: "field-keywords",
+ value: "{searchTerms}",
+ },
+ ],
+ },
+ },
+ ],
+ default: "yes-if-no-other",
+ },
+ {
+ webExtension: {
+ id: "lycos@example.com",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ {
+ override: true,
+ experiment: "experiment1",
+ sendAttributionRequest: true,
+ },
+ ],
+ default: "yes",
+ },
+ {
+ webExtension: {
+ id: "altavista@example.com",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ {
+ override: true,
+ application: {
+ distributions: ["distro2"],
+ },
+ included: { regions: ["gb"] },
+ params: {
+ searchUrlGetParams: [
+ {
+ name: "field-keywords2",
+ value: "{searchTerms}",
+ },
+ ],
+ },
+ },
+ ],
+ default: "yes",
+ },
+];
+
+const engineSelector = new SearchEngineSelector();
+
+add_task(async function setup() {
+ const settings = await RemoteSettings(SearchUtils.SETTINGS_KEY);
+ sinon.stub(settings, "get").returns(TEST_CONFIG);
+});
+
+add_task(async function test_engine_selector_defaults() {
+ // Check that with no override sections matching, we have no overrides active.
+ const { engines } = await engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "us",
+ });
+
+ let engine = engines.find(e => e.webExtension.id == "aol@example.com");
+
+ Assert.ok(
+ !("params" in engine),
+ "Should not have overriden the parameters of the engine."
+ );
+
+ engine = engines.find(e => e.webExtension.id == "lycos@example.com");
+
+ Assert.ok(
+ !("sendAttributionRequest" in engine),
+ "Should have overriden the sendAttributionRequest field of the engine."
+ );
+});
+
+add_task(async function test_engine_selector_override_distributions() {
+ const { engines } = await engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "us",
+ distroID: "distro1",
+ });
+
+ let engine = engines.find(e => e.webExtension.id == "aol@example.com");
+
+ Assert.deepEqual(
+ engine.params,
+ {
+ searchUrlGetParams: [
+ {
+ name: "field-keywords",
+ value: "{searchTerms}",
+ },
+ ],
+ },
+ "Should have overriden the parameters of the engine."
+ );
+});
+
+add_task(async function test_engine_selector_override_experiments() {
+ const { engines } = await engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "us",
+ experiment: "experiment1",
+ });
+
+ let engine = engines.find(e => e.webExtension.id == "lycos@example.com");
+
+ Assert.equal(
+ engine.sendAttributionRequest,
+ true,
+ "Should have overriden the sendAttributionRequest field of the engine."
+ );
+});
+
+add_task(async function test_engine_selector_override_with_included() {
+ let { engines } = await engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "us",
+ distroID: "distro2",
+ });
+
+ let engine = engines.find(e => e.webExtension.id == "altavista@example.com");
+ Assert.ok(
+ !("params" in engine),
+ "Should not have overriden the parameters of the engine."
+ );
+
+ let result = await engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "gb",
+ distroID: "distro2",
+ });
+ engine = result.engines.find(
+ e => e.webExtension.id == "altavista@example.com"
+ );
+ Assert.deepEqual(
+ engine.params,
+ {
+ searchUrlGetParams: [
+ {
+ name: "field-keywords2",
+ value: "{searchTerms}",
+ },
+ ],
+ },
+ "Should have overriden the parameters of the engine."
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_engine_selector_remote_settings.js b/toolkit/components/search/tests/xpcshell/test_engine_selector_remote_settings.js
new file mode 100644
index 0000000000..336ffb1ee5
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_engine_selector_remote_settings.js
@@ -0,0 +1,343 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
+ SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs",
+});
+
+const TEST_CONFIG = [
+ {
+ engineName: "aol",
+ orderHint: 500,
+ webExtension: {
+ locales: ["default"],
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ {
+ included: { regions: ["us"] },
+ webExtension: {
+ locales: ["$USER_LOCALE"],
+ },
+ },
+ ],
+ },
+ {
+ engineName: "lycos",
+ orderHint: 1000,
+ default: "yes",
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ excluded: { locales: { matches: ["zh-CN"] } },
+ },
+ ],
+ },
+ {
+ engineName: "altavista",
+ orderHint: 2000,
+ defaultPrivate: "yes",
+ appliesTo: [
+ {
+ included: { locales: { matches: ["en-US"] } },
+ },
+ {
+ included: { regions: ["default"] },
+ },
+ ],
+ },
+ {
+ engineName: "excite",
+ default: "yes-if-no-other",
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ excluded: { regions: ["us"] },
+ },
+ {
+ included: { everywhere: true },
+ cohort: "acohortid",
+ },
+ ],
+ },
+ {
+ engineName: "askjeeves",
+ },
+];
+
+let getStub;
+
+add_task(async function setup() {
+ const searchConfigSettings = await RemoteSettings(SearchUtils.SETTINGS_KEY);
+ getStub = sinon.stub(searchConfigSettings, "get");
+
+ // We expect this error from remove settings as we're invalidating the
+ // signature.
+ consoleAllowList.push("Invalid content signature (abc)");
+ // We also test returning an empty configuration.
+ consoleAllowList.push("Received empty search configuration");
+});
+
+add_task(async function test_selector_basic_get() {
+ const listenerSpy = sinon.spy();
+ const engineSelector = new SearchEngineSelector(listenerSpy);
+ getStub.onFirstCall().returns(TEST_CONFIG);
+
+ const { engines } = await engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "default",
+ });
+
+ Assert.deepEqual(
+ engines.map(e => e.engineName),
+ ["lycos", "altavista", "aol", "excite"],
+ "Should have obtained the correct data from the database."
+ );
+ Assert.ok(listenerSpy.notCalled, "Should not have called the listener");
+});
+
+add_task(async function test_selector_get_reentry() {
+ const listenerSpy = sinon.spy();
+ const engineSelector = new SearchEngineSelector(listenerSpy);
+ let promise = PromiseUtils.defer();
+ getStub.resetHistory();
+ getStub.onFirstCall().returns(promise.promise);
+ delete engineSelector._configuration;
+
+ let firstResult;
+ let secondResult;
+
+ const firstCallPromise = engineSelector
+ .fetchEngineConfiguration({
+ locale: "en-US",
+ region: "default",
+ })
+ .then(result => (firstResult = result.engines));
+
+ const secondCallPromise = engineSelector
+ .fetchEngineConfiguration({
+ locale: "en-US",
+ region: "default",
+ })
+ .then(result => (secondResult = result.engines));
+
+ Assert.strictEqual(
+ firstResult,
+ undefined,
+ "Should not have returned the first result yet."
+ );
+
+ Assert.strictEqual(
+ secondResult,
+ undefined,
+ "Should not have returned the second result yet."
+ );
+
+ promise.resolve(TEST_CONFIG);
+
+ await Promise.all([firstCallPromise, secondCallPromise]);
+ Assert.deepEqual(
+ firstResult.map(e => e.engineName),
+ ["lycos", "altavista", "aol", "excite"],
+ "Should have returned the correct data to the first call"
+ );
+
+ Assert.deepEqual(
+ secondResult.map(e => e.engineName),
+ ["lycos", "altavista", "aol", "excite"],
+ "Should have returned the correct data to the second call"
+ );
+ Assert.ok(listenerSpy.notCalled, "Should not have called the listener");
+});
+
+add_task(async function test_selector_config_update() {
+ const listenerSpy = sinon.spy();
+ const engineSelector = new SearchEngineSelector(listenerSpy);
+ getStub.resetHistory();
+ getStub.onFirstCall().returns(TEST_CONFIG);
+
+ const { engines } = await engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "default",
+ });
+
+ Assert.deepEqual(
+ engines.map(e => e.engineName),
+ ["lycos", "altavista", "aol", "excite"],
+ "Should have got the correct configuration"
+ );
+
+ Assert.ok(listenerSpy.notCalled, "Should not have called the listener yet");
+
+ const NEW_DATA = [
+ {
+ default: "yes",
+ engineName: "askjeeves",
+ appliesTo: [{ included: { everywhere: true } }],
+ schema: 1553857697843,
+ last_modified: 1553859483588,
+ },
+ ];
+
+ getStub.resetHistory();
+ getStub.onFirstCall().returns(NEW_DATA);
+ await RemoteSettings(SearchUtils.SETTINGS_KEY).emit("sync", {
+ data: {
+ current: NEW_DATA,
+ },
+ });
+
+ Assert.ok(listenerSpy.called, "Should have called the listener");
+
+ const result = await engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "default",
+ });
+
+ Assert.deepEqual(
+ result.engines.map(e => e.engineName),
+ ["askjeeves"],
+ "Should have updated the configuration with the new data"
+ );
+});
+
+add_task(async function test_selector_db_modification() {
+ const engineSelector = new SearchEngineSelector();
+ // Fill the database with some values that we can use to test that it is cleared.
+ const db = RemoteSettings(SearchUtils.SETTINGS_KEY).db;
+ await db.importChanges(
+ {},
+ Date.now(),
+ [
+ {
+ id: "85e1f268-9ca5-4b52-a4ac-922df5c07264",
+ default: "yes",
+ engineName: "askjeeves",
+ appliesTo: [{ included: { everywhere: true } }],
+ },
+ ],
+ { clear: true }
+ );
+
+ // Stub the get() so that the first call simulates a signature error, and
+ // the second simulates success reading from the dump.
+ getStub.resetHistory();
+ getStub
+ .onFirstCall()
+ .rejects(new RemoteSettingsClient.InvalidSignatureError("abc"));
+ getStub.onSecondCall().returns(TEST_CONFIG);
+
+ let result = await engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "default",
+ });
+
+ Assert.ok(
+ getStub.calledTwice,
+ "Should have called the get() function twice."
+ );
+
+ const databaseEntries = await db.list();
+ Assert.equal(databaseEntries.length, 0, "Should have cleared the database.");
+
+ Assert.deepEqual(
+ result.engines.map(e => e.engineName),
+ ["lycos", "altavista", "aol", "excite"],
+ "Should have returned the correct data."
+ );
+});
+
+add_task(async function test_selector_db_modification_never_succeeds() {
+ const engineSelector = new SearchEngineSelector();
+ // Fill the database with some values that we can use to test that it is cleared.
+ const db = RemoteSettings(SearchUtils.SETTINGS_KEY).db;
+ await db.importChanges(
+ {},
+ Date.now(),
+ [
+ {
+ id: "b70edfdd-1c3f-4b7b-ab55-38cb048636c0",
+ default: "yes",
+ engineName: "askjeeves",
+ appliesTo: [{ included: { everywhere: true } }],
+ },
+ ],
+ {
+ clear: true,
+ }
+ );
+
+ // Now simulate the condition where for some reason we never get a
+ // valid result.
+ getStub.reset();
+ getStub.rejects(new RemoteSettingsClient.InvalidSignatureError("abc"));
+
+ await Assert.rejects(
+ engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "default",
+ }),
+ ex => ex.result == Cr.NS_ERROR_UNEXPECTED,
+ "Should have rejected loading the engine configuration"
+ );
+
+ Assert.ok(
+ getStub.calledTwice,
+ "Should have called the get() function twice."
+ );
+
+ const databaseEntries = await db.list();
+ Assert.equal(databaseEntries.length, 0, "Should have cleared the database.");
+});
+
+add_task(async function test_empty_results() {
+ // Check that returning an empty result re-tries.
+ const engineSelector = new SearchEngineSelector();
+ // Fill the database with some values that we can use to test that it is cleared.
+ const db = RemoteSettings(SearchUtils.SETTINGS_KEY).db;
+ await db.importChanges(
+ {},
+ Date.now(),
+ [
+ {
+ id: "df5655ca-e045-4f8c-a7ee-047eeb654722",
+ default: "yes",
+ engineName: "askjeeves",
+ appliesTo: [{ included: { everywhere: true } }],
+ },
+ ],
+ {
+ clear: true,
+ }
+ );
+
+ // Stub the get() so that the first call simulates an empty database, and
+ // the second simulates success reading from the dump.
+ getStub.resetHistory();
+ getStub.onFirstCall().returns([]);
+ getStub.onSecondCall().returns(TEST_CONFIG);
+
+ let result = await engineSelector.fetchEngineConfiguration({
+ locale: "en-US",
+ region: "default",
+ });
+
+ Assert.ok(
+ getStub.calledTwice,
+ "Should have called the get() function twice."
+ );
+
+ const databaseEntries = await db.list();
+ Assert.equal(databaseEntries.length, 0, "Should have cleared the database.");
+
+ Assert.deepEqual(
+ result.engines.map(e => e.engineName),
+ ["lycos", "altavista", "aol", "excite"],
+ "Should have returned the correct data."
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_engine_set_alias.js b/toolkit/components/search/tests/xpcshell/test_engine_set_alias.js
new file mode 100644
index 0000000000..2732183517
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_engine_set_alias.js
@@ -0,0 +1,132 @@
+"use strict";
+
+add_task(async function setup() {
+ useHttpServer();
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+});
+
+add_task(async function test_engine_set_alias() {
+ info("Set engine alias");
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "bacon",
+ keyword: "b",
+ search_url: "https://www.bacon.test/find",
+ },
+ { skipUnload: true }
+ );
+ let engine1 = await Services.search.getEngineByName("bacon");
+ Assert.ok(engine1.aliases.includes("b"));
+ engine1.alias = "a";
+ Assert.equal(engine1.alias, "a");
+ await extension.unload();
+});
+
+add_task(async function test_engine_set_alias_with_left_space() {
+ info("Set engine alias with left space");
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "bacon",
+ keyword: " a",
+ search_url: "https://www.bacon.test/find",
+ },
+ { skipUnload: true }
+ );
+ let engine2 = await Services.search.getEngineByName("bacon");
+ Assert.ok(engine2.aliases.includes("a"));
+ engine2.alias = " c";
+ Assert.equal(engine2.alias, "c");
+ await extension.unload();
+});
+
+add_task(async function test_engine_set_alias_with_right_space() {
+ info("Set engine alias with right space");
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "bacon",
+ keyword: "c ",
+ search_url: "https://www.bacon.test/find",
+ },
+ { skipUnload: true }
+ );
+ let engine3 = await Services.search.getEngineByName("bacon");
+ Assert.ok(engine3.aliases.includes("c"));
+ engine3.alias = "o ";
+ Assert.equal(engine3.alias, "o");
+ await extension.unload();
+});
+
+add_task(async function test_engine_set_alias_with_right_left_space() {
+ info("Set engine alias with left and right space");
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "bacon",
+ keyword: " o ",
+ search_url: "https://www.bacon.test/find",
+ },
+ { skipUnload: true }
+ );
+ let engine4 = await Services.search.getEngineByName("bacon");
+ Assert.ok(engine4.aliases.includes("o"));
+ engine4.alias = " n ";
+ Assert.equal(engine4.alias, "n");
+ await extension.unload();
+});
+
+add_task(async function test_engine_set_alias_with_space() {
+ info("Set engine alias with space");
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "bacon",
+ keyword: " ",
+ search_url: "https://www.bacon.test/find",
+ },
+ { skipUnload: true }
+ );
+ let engine5 = await Services.search.getEngineByName("bacon");
+ Assert.equal(engine5.alias, "");
+ engine5.alias = "b";
+ Assert.equal(engine5.alias, "b");
+ engine5.alias = " ";
+ Assert.equal(engine5.alias, "");
+ await extension.unload();
+});
+
+add_task(async function test_engine_change_alias() {
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "bacon",
+ keyword: " o ",
+ search_url: "https://www.bacon.test/find",
+ },
+ { skipUnload: true }
+ );
+ let engine6 = await Services.search.getEngineByName("bacon");
+
+ let promise = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+
+ engine6.alias = "ba";
+
+ await promise;
+ Assert.equal(
+ engine6.alias,
+ "ba",
+ "Should have correctly notified and changed the alias."
+ );
+
+ let observed = false;
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ observed = true;
+ }, SearchUtils.TOPIC_ENGINE_MODIFIED);
+
+ engine6.alias = "ba";
+
+ Assert.equal(engine6.alias, "ba", "Should have not changed the alias");
+ Assert.ok(!observed, "Should not have notified for no change in alias");
+
+ await extension.unload();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_getSubmission_encoding.js b/toolkit/components/search/tests/xpcshell/test_getSubmission_encoding.js
new file mode 100644
index 0000000000..b868f1aa8b
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_getSubmission_encoding.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const prefix = "https://example.com/?sourceId=Mozilla-search&search=";
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("simple-engines");
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+});
+
+function testEncode(engine, charset, query, expected) {
+ engine.wrappedJSObject._queryCharset = charset;
+
+ Assert.equal(
+ engine.getSubmission(query).uri.spec,
+ prefix + expected,
+ `Should have correctly encoded for ${charset}`
+ );
+}
+
+add_task(async function test_getSubmission_encoding() {
+ let engine = await Services.search.getEngineByName("Simple Engine");
+
+ testEncode(engine, "UTF-8", "caff\u00E8", "caff%C3%A8");
+ testEncode(engine, "windows-1252", "caff\u00E8", "caff%E8");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_getSubmission_params.js b/toolkit/components/search/tests/xpcshell/test_getSubmission_params.js
new file mode 100644
index 0000000000..648267e5fc
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_getSubmission_params.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("simple-engines");
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+});
+
+const searchTerms = "fxsearch";
+function checkSubstitution(url, prefix, engine, template, expected) {
+ url.template = prefix + template;
+ equal(engine.getSubmission(searchTerms).uri.spec, prefix + expected);
+}
+
+add_task(async function test_paramSubstitution() {
+ let prefix = "https://example.com/?sourceId=Mozilla-search&search=";
+ let engine = await Services.search.getEngineByName("Simple Engine");
+ let url = engine.wrappedJSObject._getURLOfType("text/html");
+ equal(url.getSubmission("foo", engine).uri.spec, prefix + "foo");
+ // Reset the engine parameters so we can have a clean template to use for
+ // the subsequent tests.
+ url.params = [];
+
+ let check = checkSubstitution.bind(this, url, prefix, engine);
+
+ // The same parameter can be used more than once.
+ check("{searchTerms}/{searchTerms}", searchTerms + "/" + searchTerms);
+
+ // Optional parameters are replaced if we known them.
+ check("{searchTerms?}", searchTerms);
+ check("{unknownOptional?}", "");
+ check("{unknownRequired}", "{unknownRequired}");
+
+ check("{language}", Services.locale.requestedLocale);
+ check("{language?}", Services.locale.requestedLocale);
+
+ engine.wrappedJSObject._queryCharset = "UTF-8";
+ check("{inputEncoding}", "UTF-8");
+ check("{inputEncoding?}", "UTF-8");
+ check("{outputEncoding}", "UTF-8");
+ check("{outputEncoding?}", "UTF-8");
+
+ // 'Unsupported' parameters with hard coded values used only when the parameter is required.
+ check("{count}", "20");
+ check("{count?}", "");
+ check("{startIndex}", "1");
+ check("{startIndex?}", "");
+ check("{startPage}", "1");
+ check("{startPage?}", "");
+
+ check("{moz:locale}", Services.locale.requestedLocale);
+
+ url.template = prefix + "{moz:date}";
+ let params = new URLSearchParams(engine.getSubmission(searchTerms).uri.query);
+ Assert.ok(params.has("search"), "Should have a search option");
+
+ let [, year, month, day, hour] = params
+ .get("search")
+ .match(/^(\d{4})(\d{2})(\d{2})(\d{2})/);
+ let date = new Date(year, month - 1, day, hour);
+
+ // We check the time is within an hour of now as the parameter is only
+ // precise to an hour. Checking the difference also should cope with date
+ // changes etc.
+ let difference = Date.now() - date;
+ Assert.lessOrEqual(
+ difference,
+ 60 * 60 * 1000,
+ "Should have set the date within an hour"
+ );
+ Assert.greaterOrEqual(difference, 0, "Should not have a time in the past.");
+});
+
+add_task(async function test_mozParamsFailForNonAppProvided() {
+ await SearchTestUtils.installSearchExtension();
+
+ let prefix = "https://example.com/?q=";
+ let engine = await Services.search.getEngineByName("Example");
+ let url = engine.wrappedJSObject._getURLOfType("text/html");
+ equal(url.getSubmission("foo", engine).uri.spec, prefix + "foo");
+ // Reset the engine parameters so we can have a clean template to use for
+ // the subsequent tests.
+ url.params = [];
+
+ let check = checkSubstitution.bind(this, url, prefix, engine);
+
+ // Test moz: parameters (only supported for built-in engines, ie _isDefault == true).
+ check("{moz:locale}", "{moz:locale}");
+
+ await promiseAfterSettings();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_identifiers.js b/toolkit/components/search/tests/xpcshell/test_identifiers.js
new file mode 100644
index 0000000000..72fa052211
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_identifiers.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test of a search engine's identifier.
+ */
+
+"use strict";
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("simple-engines");
+ await AddonTestUtils.promiseStartupManager();
+
+ const result = await Services.search.init();
+ Assert.ok(
+ Components.isSuccessCode(result),
+ "Should have initialized the service"
+ );
+
+ useHttpServer();
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engine.xml`,
+ });
+});
+
+function checkIdentifier(engineName, expectedIdentifier, expectedTelemetryId) {
+ const engine = Services.search.getEngineByName(engineName);
+ Assert.ok(
+ engine instanceof Ci.nsISearchEngine,
+ "Should be derived from nsISearchEngine"
+ );
+
+ Assert.equal(
+ engine.identifier,
+ expectedIdentifier,
+ "Should have the correct identifier"
+ );
+
+ Assert.equal(
+ engine.telemetryId,
+ expectedTelemetryId,
+ "Should have the correct telemetry Id"
+ );
+}
+
+add_task(async function test_from_profile() {
+ // An engine loaded from the profile directory won't have an identifier,
+ // because it's not built-in.
+ checkIdentifier(kTestEngineName, null, `other-${kTestEngineName}`);
+});
+
+add_task(async function test_from_telemetry_id() {
+ checkIdentifier("basic", "telemetry", "telemetry");
+});
+
+add_task(async function test_from_webextension_id() {
+ // If not specified, the telemetry Id is derived from the WebExtension prefix,
+ // it should not use the WebExtension display name.
+ checkIdentifier("Simple Engine", "simple", "simple");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_ignorelist.js b/toolkit/components/search/tests/xpcshell/test_ignorelist.js
new file mode 100644
index 0000000000..cf00e7ca30
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_ignorelist.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const kSearchEngineID1 = "ignorelist_test_engine1";
+const kSearchEngineID2 = "ignorelist_test_engine2";
+const kSearchEngineID3 = "ignorelist_test_engine3";
+const kSearchEngineURL1 =
+ "https://example.com/?search={searchTerms}&ignore=true";
+const kSearchEngineURL2 =
+ "https://example.com/?search={searchTerms}&IGNORE=TRUE";
+const kSearchEngineURL3 = "https://example.com/?search={searchTerms}";
+const kExtensionID = "searchignore@mozilla.com";
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+});
+
+add_task(async function test_ignoreList() {
+ await setupRemoteSettings();
+
+ Assert.ok(
+ !Services.search.isInitialized,
+ "Search service should not be initialized to begin with."
+ );
+
+ let updatePromise = SearchTestUtils.promiseSearchNotification(
+ "settings-update-complete"
+ );
+
+ await SearchTestUtils.installSearchExtension({
+ name: kSearchEngineID1,
+ search_url: kSearchEngineURL1,
+ });
+
+ await updatePromise;
+
+ let engine = Services.search.getEngineByName(kSearchEngineID1);
+ Assert.equal(
+ engine,
+ null,
+ "Engine with ignored search params should not exist"
+ );
+
+ await SearchTestUtils.installSearchExtension({
+ name: kSearchEngineID2,
+ search_url: kSearchEngineURL2,
+ });
+
+ // An ignored engine shouldn't be available at all
+ engine = Services.search.getEngineByName(kSearchEngineID2);
+ Assert.equal(
+ engine,
+ null,
+ "Engine with ignored search params of a different case should not exist"
+ );
+
+ await SearchTestUtils.installSearchExtension({
+ id: kExtensionID,
+ name: kSearchEngineID3,
+ search_url: kSearchEngineURL3,
+ });
+
+ // An ignored engine shouldn't be available at all
+ engine = Services.search.getEngineByName(kSearchEngineID3);
+ Assert.equal(
+ engine,
+ null,
+ "Engine with ignored extension id should not exist"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_ignorelist_update.js b/toolkit/components/search/tests/xpcshell/test_ignorelist_update.js
new file mode 100644
index 0000000000..4a64bd469e
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_ignorelist_update.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const kSearchEngineID1 = "ignorelist_test_engine1";
+const kSearchEngineID2 = "ignorelist_test_engine2";
+const kSearchEngineID3 = "ignorelist_test_engine3";
+const kSearchEngineURL1 =
+ "https://example.com/?search={searchTerms}&ignore=true";
+const kSearchEngineURL2 =
+ "https://example.com/?search={searchTerms}&IGNORE=TRUE";
+const kSearchEngineURL3 = "https://example.com/?search={searchTerms}";
+const kExtensionID = "searchignore@mozilla.com";
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+});
+
+add_task(async function test_ignoreList() {
+ Assert.ok(
+ !Services.search.isInitialized,
+ "Search service should not be initialized to begin with."
+ );
+
+ let updatePromise = SearchTestUtils.promiseSearchNotification(
+ "settings-update-complete"
+ );
+ await SearchTestUtils.installSearchExtension({
+ name: kSearchEngineID1,
+ search_url: kSearchEngineURL1,
+ });
+ await SearchTestUtils.installSearchExtension({
+ name: kSearchEngineID2,
+ search_url: kSearchEngineURL2,
+ });
+ await SearchTestUtils.installSearchExtension({
+ id: kExtensionID,
+ name: kSearchEngineID3,
+ search_url: kSearchEngineURL3,
+ });
+
+ // Ensure that the initial remote settings update from default values is
+ // complete. The defaults do not include the special inclusions inserted below.
+ await updatePromise;
+
+ for (let engineName of [
+ kSearchEngineID1,
+ kSearchEngineID2,
+ kSearchEngineID3,
+ ]) {
+ Assert.ok(
+ await Services.search.getEngineByName(engineName),
+ `Engine ${engineName} should be present`
+ );
+ }
+
+ // Simulate an ignore list update.
+ await RemoteSettings("hijack-blocklists").emit("sync", {
+ data: {
+ current: [
+ {
+ id: "load-paths",
+ schema: 1553857697843,
+ last_modified: 1553859483588,
+ matches: ["[addon]searchignore@mozilla.com"],
+ },
+ {
+ id: "submission-urls",
+ schema: 1553857697843,
+ last_modified: 1553859435500,
+ matches: ["ignore=true"],
+ },
+ ],
+ },
+ });
+
+ for (let engineName of [
+ kSearchEngineID1,
+ kSearchEngineID2,
+ kSearchEngineID3,
+ ]) {
+ Assert.equal(
+ await Services.search.getEngineByName(engineName),
+ null,
+ `Engine ${engineName} should not be present`
+ );
+ }
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_initialization.js b/toolkit/components/search/tests/xpcshell/test_initialization.js
new file mode 100644
index 0000000000..99505bdeb5
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_initialization.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that a delayed add-on manager start up does not affect the start up
+// of the search service.
+
+"use strict";
+
+const CONFIG = [
+ {
+ webExtension: {
+ id: "engine@search.mozilla.org",
+ },
+ orderHint: 30,
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ default: "yes",
+ },
+ ],
+ },
+];
+
+add_setup(() => {
+ do_get_profile();
+ Services.fog.initializeFOG();
+});
+
+add_task(async function test_initialization_delayed_addon_manager() {
+ let stub = await SearchTestUtils.useTestEngines("data", null, CONFIG);
+ // Wait until the search service gets its configuration before starting
+ // to initialise the add-on manager. This simulates the add-on manager
+ // starting late which used to cause the search service to fail to load any
+ // engines.
+ stub.callsFake(() => {
+ Services.tm.dispatchToMainThread(() => {
+ AddonTestUtils.promiseStartupManager();
+ });
+ return CONFIG;
+ });
+
+ await Services.search.init();
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "Test search engine",
+ "Test engine shouldn't be the default anymore"
+ );
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "engine",
+ displayName: "Test search engine",
+ loadPath: "[addon]engine@search.mozilla.org",
+ submissionUrl: "https://www.google.com/search?q=",
+ verified: "default",
+ },
+ });
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_initialization_with_region.js b/toolkit/components/search/tests/xpcshell/test_initialization_with_region.js
new file mode 100644
index 0000000000..54a5adfc48
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_initialization_with_region.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const SEARCH_SERVICE_TOPIC = "browser-search-service";
+const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
+
+const CONFIG = [
+ {
+ webExtension: {
+ id: "engine@search.mozilla.org",
+ },
+ orderHint: 30,
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ excluded: { regions: ["FR"] },
+ default: "yes",
+ defaultPrivate: "yes",
+ },
+ ],
+ },
+ {
+ webExtension: {
+ id: "engine-pref@search.mozilla.org",
+ },
+ orderHint: 20,
+ appliesTo: [
+ {
+ included: { regions: ["FR"] },
+ default: "yes",
+ defaultPrivate: "yes",
+ },
+ ],
+ },
+];
+
+// Default engine with no region defined.
+const DEFAULT = "Test search engine";
+// Default engine with region set to FR.
+const FR_DEFAULT = "engine-pref";
+
+function listenFor(name, key) {
+ let notifyObserved = false;
+ let obs = (subject, topic, data) => {
+ if (data == key) {
+ notifyObserved = true;
+ }
+ };
+ Services.obs.addObserver(obs, name);
+
+ return () => {
+ Services.obs.removeObserver(obs, name);
+ return notifyObserved;
+ };
+}
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref("browser.search.separatePrivateDefault", true);
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ true
+ );
+
+ SearchTestUtils.useMockIdleService();
+ await SearchTestUtils.useTestEngines("data", null, CONFIG);
+ await AddonTestUtils.promiseStartupManager();
+});
+
+// This tests what we expect is the normal startup route for a fresh profile -
+// the search service initializes with no region details, then gets a region
+// notified part way through / afterwards.
+add_task(async function test_initialization_with_region() {
+ let reloadObserved = listenFor(SEARCH_SERVICE_TOPIC, "engines-reloaded");
+ let initPromise;
+
+ // Ensure the region lookup completes after init so the
+ // engines are reloaded
+ let srv = useHttpServer();
+ srv.registerPathHandler("/fetch_region", async (req, res) => {
+ res.processAsync();
+ await initPromise;
+ res.setStatusLine("1.1", 200, "OK");
+ res.write(JSON.stringify({ country_code: "FR" }));
+ res.finish();
+ });
+
+ Services.prefs.setCharPref(
+ "browser.region.network.url",
+ `http://localhost:${srv.identity.primaryPort}/fetch_region`
+ );
+
+ Region._setHomeRegion("", false);
+ Region.init();
+
+ initPromise = Services.search.init();
+ await initPromise;
+
+ let otherPromises = [
+ // This test expects settings to be saved twice.
+ promiseAfterSettings().then(promiseAfterSettings),
+ SearchTestUtils.promiseSearchNotification(
+ "engine-default",
+ SEARCH_ENGINE_TOPIC
+ ),
+ ];
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ DEFAULT,
+ "Test engine shouldn't be the default anymore"
+ );
+
+ await Promise.all(otherPromises);
+
+ // Ensure that correct engine is being reported as the default.
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ FR_DEFAULT,
+ "engine-pref should be the default in FR"
+ );
+ Assert.equal(
+ (await Services.search.getDefaultPrivate()).name,
+ FR_DEFAULT,
+ "engine-pref should be the private default in FR"
+ );
+
+ Assert.ok(reloadObserved(), "Engines do reload with delayed region fetch");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_list_json_locale.js b/toolkit/components/search/tests/xpcshell/test_list_json_locale.js
new file mode 100644
index 0000000000..a02e07b9d2
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_list_json_locale.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Check default search engine is picked from list.json searchDefault */
+
+"use strict";
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines();
+
+ Services.locale.availableLocales = [
+ ...Services.locale.availableLocales,
+ "de",
+ "fr",
+ ];
+
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ true
+ );
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+ Region._setHomeRegion("US", false);
+});
+
+add_task(async function test_listJSONlocale() {
+ Services.locale.requestedLocales = ["de"];
+
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+
+ Assert.ok(Services.search.isInitialized, "search initialized");
+
+ let sortedEngines = await Services.search.getEngines();
+ Assert.equal(sortedEngines.length, 1, "Should have only one engine");
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "Test search engine",
+ "Should have the correct default engine"
+ );
+ Assert.equal(
+ Services.search.defaultPrivateEngine.name,
+ // 'de' only displays google, so we'll be using the same engine as the
+ // normal default.
+ "Test search engine",
+ "Should have the correct private default engine"
+ );
+});
+
+// Check that switching locale switches search engines
+add_task(async function test_listJSONlocaleSwitch() {
+ let defaultBranch = Services.prefs.getDefaultBranch(
+ SearchUtils.BROWSER_SEARCH_PREF
+ );
+ defaultBranch.setCharPref("param.code", "good&id=unique");
+
+ await promiseSetLocale("fr");
+
+ Assert.ok(Services.search.isInitialized, "search initialized");
+
+ let sortedEngines = await Services.search.getEngines();
+ Assert.deepEqual(
+ sortedEngines.map(e => e.name),
+ ["Test search engine", "engine-pref", "engine-resourceicon"],
+ "Should have the correct engine list"
+ );
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "Test search engine",
+ "Should have the correct default engine"
+ );
+ Assert.equal(
+ Services.search.defaultPrivateEngine.name,
+ "engine-pref",
+ "Should have the correct private default engine"
+ );
+});
+
+// Check that region overrides apply
+add_task(async function test_listJSONRegionOverride() {
+ await promiseSetHomeRegion("RU");
+
+ Assert.ok(Services.search.isInitialized, "search initialized");
+
+ let sortedEngines = await Services.search.getEngines();
+ Assert.deepEqual(
+ sortedEngines.map(e => e.name),
+ ["Test search engine", "engine-pref", "engine-chromeicon"],
+ "Should have the correct engine list"
+ );
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "Test search engine",
+ "Should have the correct default engine"
+ );
+ Assert.equal(
+ Services.search.defaultPrivateEngine.name,
+ "engine-pref",
+ "Should have the correct private default engine"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_list_json_no_private_default.js b/toolkit/components/search/tests/xpcshell/test_list_json_no_private_default.js
new file mode 100644
index 0000000000..8de8ad6a34
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_list_json_no_private_default.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// TODO: Test fallback to normal default when no private set at all.
+
+/* Check default search engine is picked from list.json searchDefault */
+
+"use strict";
+
+// Check that current engine matches with US searchDefault from list.json
+add_task(async function test_searchDefaultEngineUS() {
+ await SearchTestUtils.useTestEngines("data1");
+
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ true
+ );
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+ Services.prefs.setCharPref(SearchUtils.BROWSER_SEARCH_PREF + "region", "US");
+
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+
+ Assert.ok(Services.search.isInitialized, "search initialized");
+
+ Assert.equal(
+ Services.search.appDefaultEngine.name,
+ "engine1",
+ "Should have the expected engine as app default"
+ );
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine1",
+ "Should have the expected engine as default"
+ );
+ Assert.equal(
+ Services.search.appPrivateDefaultEngine.name,
+ "engine1",
+ "Should have the same engine for the app private default"
+ );
+ Assert.equal(
+ Services.search.defaultPrivateEngine.name,
+ "engine1",
+ "Should have the same engine for the private default"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_list_json_searchdefault.js b/toolkit/components/search/tests/xpcshell/test_list_json_searchdefault.js
new file mode 100644
index 0000000000..38c7f302d2
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_list_json_searchdefault.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Check default search engine is picked from list.json searchDefault */
+
+"use strict";
+
+// Check that current engine matches with US searchDefault from list.json
+add_task(async function test_searchDefaultEngineUS() {
+ await SearchTestUtils.useTestEngines();
+
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ true
+ );
+
+ Services.prefs.setCharPref(SearchUtils.BROWSER_SEARCH_PREF + "region", "US");
+
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+
+ Assert.ok(Services.search.isInitialized, "search initialized");
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "Test search engine",
+ "Should have the expected engine as default."
+ );
+ Assert.equal(
+ Services.search.appDefaultEngine.name,
+ "Test search engine",
+ "Should have the expected engine as the app default"
+ );
+
+ // First with the pref off to check using the existing values.
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ false
+ );
+
+ Assert.equal(
+ Services.search.defaultPrivateEngine.name,
+ Services.search.defaultEngine.name,
+ "Should have the normal default engine when separate private browsing is off."
+ );
+ Assert.equal(
+ Services.search.appPrivateDefaultEngine.name,
+ Services.search.appDefaultEngine.name,
+ "Should have the normal app engine when separate private browsing is off."
+ );
+
+ // Then with the pref on.
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+
+ Assert.equal(
+ Services.search.defaultPrivateEngine.name,
+ "engine-pref",
+ "Should have the private default engine when separate private browsing is on."
+ );
+ Assert.equal(
+ Services.search.appPrivateDefaultEngine.name,
+ "engine-pref",
+ "Should have the app private engine set correctly when separate private browsing is on."
+ );
+
+ Services.prefs.clearUserPref("browser.search.region");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_list_json_searchorder.js b/toolkit/components/search/tests/xpcshell/test_list_json_searchorder.js
new file mode 100644
index 0000000000..21e5b60f0f
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_list_json_searchorder.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Check default search engine is picked from list.json searchDefault */
+
+"use strict";
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ true
+ );
+
+ await SearchTestUtils.useTestEngines();
+ await Services.search.init();
+});
+
+async function checkOrder(expectedOrder) {
+ const sortedEngines = await Services.search.getEngines();
+ Assert.deepEqual(
+ sortedEngines.map(s => s.name),
+ expectedOrder,
+ "Should have the expected engine order"
+ );
+}
+
+add_task(async function test_searchOrderJSON_no_separate_private() {
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ false
+ );
+
+ await checkOrder([
+ // Default engine
+ "Test search engine",
+ // Two engines listed in searchOrder.
+ "engine-resourceicon",
+ "engine-chromeicon",
+ // Rest of the engines in order.
+ "engine-pref",
+ "engine-rel-searchform-purpose",
+ "Test search engine (Reordered)",
+ ]);
+});
+
+add_task(async function test_searchOrderJSON_separate_private() {
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+
+ await checkOrder([
+ // Default engine
+ "Test search engine",
+ // Default private engine
+ "engine-pref",
+ // Two engines listed in searchOrder.
+ "engine-resourceicon",
+ "engine-chromeicon",
+ // Rest of the engines in order.
+ "engine-rel-searchform-purpose",
+ "Test search engine (Reordered)",
+ ]);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_location_timeout_xhr.js b/toolkit/components/search/tests/xpcshell/test_location_timeout_xhr.js
new file mode 100644
index 0000000000..d76d775dd5
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_timeout_xhr.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This is testing the long, last-resort XHR-based timeout for the location
+// search.
+
+function startServer(continuePromise) {
+ let srv = new HttpServer();
+ function lookupCountry(metadata, response) {
+ response.processAsync();
+ // wait for our continuePromise to resolve before writing a valid
+ // response.
+ // This will be resolved after the timeout period, so we can check
+ // the behaviour in that case.
+ continuePromise.then(() => {
+ response.setStatusLine("1.1", 200, "OK");
+ response.write('{"country_code" : "AU"}');
+ response.finish();
+ });
+ }
+ srv.registerPathHandler("/lookup_country", lookupCountry);
+ srv.start(-1);
+ return srv;
+}
+
+function verifyProbeSum(probe, sum) {
+ let histogram = Services.telemetry.getHistogramById(probe);
+ let snapshot = histogram.snapshot();
+ equal(snapshot.sum, sum, probe);
+}
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+});
+
+add_task(async function test_location_timeout_xhr() {
+ let resolveContinuePromise;
+ let continuePromise = new Promise(resolve => {
+ resolveContinuePromise = resolve;
+ });
+
+ let server = startServer(continuePromise);
+ let url =
+ "http://localhost:" + server.identity.primaryPort + "/lookup_country";
+ Services.prefs.setCharPref("browser.search.geoip.url", url);
+ // The timeout for the timer.
+ Services.prefs.setIntPref("browser.search.geoip.timeout", 10);
+ let promiseXHRStarted = SearchTestUtils.promiseSearchNotification(
+ "geoip-lookup-xhr-starting"
+ );
+ await Services.search.init();
+ ok(
+ !Services.prefs.prefHasUserValue("browser.search.region"),
+ "should be no region pref"
+ );
+ // should be no result recorded at all.
+ checkCountryResultTelemetry(null);
+
+ // should not have SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS recorded as our
+ // test server is still blocked on our promise.
+ verifyProbeSum("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS", 0);
+
+ promiseXHRStarted.then(xhr => {
+ // Set the timeout on the xhr object to an extremely low value, so it
+ // should timeout immediately.
+ xhr.timeout = 10;
+ // wait for the xhr timeout to fire.
+ SearchTestUtils.promiseSearchNotification("geoip-lookup-xhr-complete").then(
+ () => {
+ // should have the XHR timeout recorded.
+ checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.TIMEOUT);
+ // still should not have a report of how long the response took as we
+ // only record that on success responses.
+ verifyProbeSum("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS", 0);
+ // and we still don't know the country code or region.
+ ok(
+ !Services.prefs.prefHasUserValue("browser.search.region"),
+ "should be no region pref"
+ );
+
+ // unblock the server even though nothing is listening.
+ resolveContinuePromise();
+
+ return new Promise(resolve => {
+ server.stop(resolve);
+ });
+ }
+ );
+ });
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_maybereloadengine_order.js b/toolkit/components/search/tests/xpcshell/test_maybereloadengine_order.js
new file mode 100644
index 0000000000..8d0a0891b8
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_maybereloadengine_order.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_CONFIG = [
+ {
+ webExtension: { id: "plainengine@search.mozilla.org" },
+ appliesTo: [{ included: { everywhere: true } }],
+ },
+ {
+ webExtension: { id: "special-engine@search.mozilla.org" },
+ appliesTo: [{ default: "yes", included: { regions: ["FR"] } }],
+ },
+];
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("test-extensions", null, TEST_CONFIG);
+ await AddonTestUtils.promiseStartupManager();
+
+ registerCleanupFunction(AddonTestUtils.promiseShutdownManager);
+});
+
+add_task(async function basic_multilocale_test() {
+ let resolver;
+ let initPromise = new Promise(resolve => (resolver = resolve));
+ useCustomGeoServer("FR", initPromise);
+
+ await Services.search.init();
+ await Services.search.getAppProvidedEngines();
+ resolver();
+ await SearchTestUtils.promiseSearchNotification("engines-reloaded");
+
+ let engines = await Services.search.getAppProvidedEngines();
+
+ Assert.deepEqual(
+ engines.map(e => e._name),
+ ["Special", "Plain"],
+ "Special engine is default so should be first"
+ );
+
+ engines.forEach(engine => {
+ Assert.ok(!engine._metaData.order, "Order is not defined");
+ });
+
+ Assert.equal(
+ Services.search.wrappedJSObject._settings.getMetaDataAttribute(
+ "useSavedOrder"
+ ),
+ false,
+ "We should not set the engine order during maybeReloadEngines"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_migrateWebExtensionEngine.js b/toolkit/components/search/tests/xpcshell/test_migrateWebExtensionEngine.js
new file mode 100644
index 0000000000..65de325924
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_migrateWebExtensionEngine.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const kExtensionID = "simple@tests.mozilla.org";
+
+add_task(async function setup() {
+ useHttpServer("opensearch");
+ await AddonTestUtils.promiseStartupManager();
+ await SearchTestUtils.useTestEngines("data1");
+ await Services.search.init();
+});
+
+add_task(async function test_migrateLegacyEngine() {
+ await Services.search.addOpenSearchEngine(gDataUrl + "simple.xml", null);
+
+ // Modify the loadpath so it looks like a legacy plugin loadpath
+ let engine = Services.search.getEngineByName("simple");
+ engine.wrappedJSObject._loadPath = `jar:[profile]/extensions/${kExtensionID}.xpi!/simple.xml`;
+ engine.wrappedJSObject._extensionID = null;
+
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ // This should replace the existing engine
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ id: "simple",
+ name: "simple",
+ search_url: "https://example.com/",
+ },
+ { skipUnload: true }
+ );
+
+ engine = Services.search.getEngineByName("simple");
+ Assert.equal(engine.wrappedJSObject._loadPath, "[addon]" + kExtensionID);
+ Assert.equal(engine.wrappedJSObject._extensionID, kExtensionID);
+
+ Assert.equal(
+ (await Services.search.getDefault()).name,
+ "simple",
+ "Should have kept the default engine the same"
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_migrateLegacyEngineDifferentName() {
+ await Services.search.addOpenSearchEngine(gDataUrl + "simple.xml", null);
+
+ // Modify the loadpath so it looks like an legacy plugin loadpath
+ let engine = Services.search.getEngineByName("simple");
+ engine.wrappedJSObject._loadPath = `jar:[profile]/extensions/${kExtensionID}.xpi!/simple.xml`;
+ engine.wrappedJSObject._extensionID = null;
+
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ // This should replace the existing engine - it has the same id, but a different name.
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ id: "simple",
+ name: "simple search",
+ search_url: "https://example.com/",
+ },
+ { skipUnload: true }
+ );
+
+ engine = Services.search.getEngineByName("simple");
+ Assert.equal(engine, null, "Should have removed the old engine");
+
+ // The engine should have changed its name.
+ engine = Services.search.getEngineByName("simple search");
+ Assert.equal(engine.wrappedJSObject._loadPath, "[addon]" + kExtensionID);
+ Assert.equal(engine.wrappedJSObject._extensionID, kExtensionID);
+
+ Assert.equal(
+ (await Services.search.getDefault()).name,
+ "simple search",
+ "Should have made the new engine default"
+ );
+
+ await extension.unload();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_missing_engine.js b/toolkit/components/search/tests/xpcshell/test_missing_engine.js
new file mode 100644
index 0000000000..9be8c79ae8
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_missing_engine.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test is designed to check the search service keeps working if there's
+// a built-in engine missing from the configuration.
+
+"use strict";
+
+const GOOD_CONFIG = [
+ {
+ webExtension: {
+ id: "engine@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ ],
+ },
+];
+
+const BAD_CONFIG = [
+ ...GOOD_CONFIG,
+ {
+ webExtension: {
+ id: "engine-missing@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ ],
+ },
+];
+
+add_task(async function setup() {
+ SearchTestUtils.useMockIdleService();
+ await AddonTestUtils.promiseStartupManager();
+
+ // This test purposely attempts to load a missing engine.
+ consoleAllowList.push(
+ "Could not load engine engine-missing@search.mozilla.org"
+ );
+});
+
+add_task(async function test_startup_with_missing() {
+ await SearchTestUtils.useTestEngines("data", null, BAD_CONFIG);
+
+ const result = await Services.search.init();
+ Assert.ok(
+ Components.isSuccessCode(result),
+ "Should have started the search service successfully."
+ );
+
+ const engines = await Services.search.getEngines();
+
+ Assert.deepEqual(
+ engines.map(e => e.name),
+ ["Test search engine"],
+ "Should have listed just the good engine"
+ );
+});
+
+add_task(async function test_update_with_missing() {
+ let reloadObserved =
+ SearchTestUtils.promiseSearchNotification("engines-reloaded");
+
+ await RemoteSettings(SearchUtils.SETTINGS_KEY).emit("sync", {
+ data: {
+ current: GOOD_CONFIG,
+ },
+ });
+
+ SearchTestUtils.idleService._fireObservers("idle");
+
+ await reloadObserved;
+
+ const engines = await Services.search.getEngines();
+
+ Assert.deepEqual(
+ engines.map(e => e.name),
+ ["Test search engine"],
+ "Should have just the good engine"
+ );
+
+ reloadObserved =
+ SearchTestUtils.promiseSearchNotification("engines-reloaded");
+
+ await RemoteSettings(SearchUtils.SETTINGS_KEY).emit("sync", {
+ data: {
+ current: BAD_CONFIG,
+ },
+ });
+
+ SearchTestUtils.idleService._fireObservers("idle");
+
+ await reloadObserved;
+
+ Assert.deepEqual(
+ engines.map(e => e.name),
+ ["Test search engine"],
+ "Should still have just the good engine"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_multipleIcons.js b/toolkit/components/search/tests/xpcshell/test_multipleIcons.js
new file mode 100644
index 0000000000..dc9cc24add
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_multipleIcons.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests getIcons() and getIconURLBySize() on engine with multiple icons.
+ */
+
+"use strict";
+
+add_task(async function setup() {
+ useHttpServer();
+ await AddonTestUtils.promiseStartupManager();
+});
+
+add_task(async function test_multipleIcons() {
+ let engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engineImages.xml`,
+ });
+
+ info("The default should be the 16x16 icon");
+ Assert.ok(engine.iconURI.spec.includes("ico16"));
+
+ Assert.ok(engine.getIconURLBySize(16, 16).includes("ico16"));
+ Assert.ok(engine.getIconURLBySize(32, 32).includes("ico32"));
+ Assert.ok(engine.getIconURLBySize(74, 74).includes("ico74"));
+
+ info("Invalid dimensions should return null.");
+ Assert.equal(null, engine.getIconURLBySize(50, 50));
+
+ let allIcons = engine.getIcons();
+
+ info("Check that allIcons contains expected icon sizes");
+ Assert.equal(allIcons.length, 3);
+ let expectedWidths = [16, 32, 74];
+ Assert.ok(
+ allIcons.every(item => {
+ let width = item.width;
+ Assert.notEqual(expectedWidths.indexOf(width), -1);
+ Assert.equal(width, item.height);
+
+ let icon = item.url.split(",").pop();
+ Assert.equal(icon, "ico" + width);
+
+ return true;
+ })
+ );
+});
+
+add_task(async function test_icon_not_in_file() {
+ let engineUrl = gDataUrl + "engine-fr.xml";
+ let engine = await Services.search.addOpenSearchEngine(
+ engineUrl,
+ "data:image/x-icon;base64,ico16"
+ );
+
+ // Even though the icon wasn't specified inside the XML file, it should be
+ // available both in the iconURI attribute and with getIconURLBySize.
+ Assert.ok(engine.iconURI.spec.includes("ico16"));
+ Assert.ok(engine.getIconURLBySize(16, 16).includes("ico16"));
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_nodb_pluschanges.js b/toolkit/components/search/tests/xpcshell/test_nodb_pluschanges.js
new file mode 100644
index 0000000000..155079ce4b
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_nodb_pluschanges.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * test_nodb: Start search service without existing settings file.
+ *
+ * Ensure that :
+ * - nothing explodes;
+ * - if we change the order, search.json.mozlz4 is updated;
+ * - this search.json.mozlz4 can be parsed;
+ * - the order stored in search.json.mozlz4 is consistent.
+ *
+ * Notes:
+ * - we install the search engines of test "test_downloadAndAddEngines.js"
+ * to ensure that this test is independent from locale, commercial agreements
+ * and configuration of Firefox.
+ */
+
+add_task(async function setup() {
+ useHttpServer();
+ await AddonTestUtils.promiseStartupManager();
+});
+
+add_task(async function test_nodb_pluschanges() {
+ let engine1 = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engine.xml`,
+ });
+ let engine2 = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engine2.xml`,
+ });
+ await promiseAfterSettings();
+
+ let search = Services.search;
+
+ await search.moveEngine(engine1, 0);
+ await search.moveEngine(engine2, 1);
+
+ // This is needed to avoid some reentrency issues in nsSearchService.
+ info("Next step is forcing flush");
+ await new Promise(resolve => executeSoon(resolve));
+
+ info("Forcing flush");
+ let promiseCommit = promiseAfterSettings();
+ search.QueryInterface(Ci.nsIObserver).observe(null, "quit-application", "");
+ await promiseCommit;
+ info("Commit complete");
+
+ // Check that the entries are placed as specified correctly
+ let metadata = await promiseEngineMetadata();
+ Assert.equal(metadata["Test search engine"].order, 1);
+ Assert.equal(metadata["A second test engine"].order, 2);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_notifications.js b/toolkit/components/search/tests/xpcshell/test_notifications.js
new file mode 100644
index 0000000000..6f7ee3243a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_notifications.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let engine;
+let appDefaultEngine;
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+ useHttpServer();
+
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ true
+ );
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+
+ appDefaultEngine = await Services.search.getDefault();
+});
+
+add_task(async function test_addingEngine_opensearch() {
+ const addEngineObserver = new SearchObserver(
+ [
+ // engine-loaded
+ // Engine was loaded.
+ SearchUtils.MODIFIED_TYPE.LOADED,
+ // engine-added
+ // Engine was added to the store by the search service.
+ SearchUtils.MODIFIED_TYPE.ADDED,
+ ],
+ SearchUtils.MODIFIED_TYPE.ADDED
+ );
+
+ await Services.search.addOpenSearchEngine(gDataUrl + "engine.xml", null);
+
+ engine = await addEngineObserver.promise;
+
+ let retrievedEngine = Services.search.getEngineByName("Test search engine");
+ Assert.equal(engine, retrievedEngine);
+});
+
+add_task(async function test_addingEngine_webExtension() {
+ const addEngineObserver = new SearchObserver(
+ [
+ // engine-added
+ // Engine was added to the store by the search service.
+ SearchUtils.MODIFIED_TYPE.ADDED,
+ ],
+ SearchUtils.MODIFIED_TYPE.ADDED
+ );
+
+ await SearchTestUtils.installSearchExtension({
+ name: "Example Engine",
+ });
+
+ let webExtensionEngine = await addEngineObserver.promise;
+
+ let retrievedEngine = Services.search.getEngineByName("Example Engine");
+ Assert.equal(webExtensionEngine, retrievedEngine);
+});
+
+async function defaultNotificationTest(
+ setPrivateDefault,
+ expectNotificationForPrivate
+) {
+ const defaultObserver = new SearchObserver([
+ expectNotificationForPrivate
+ ? SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
+ : SearchUtils.MODIFIED_TYPE.DEFAULT,
+ ]);
+
+ Services.search[
+ setPrivateDefault ? "defaultPrivateEngine" : "defaultEngine"
+ ] = engine;
+ await defaultObserver.promise;
+}
+
+add_task(async function test_defaultEngine_notifications() {
+ await defaultNotificationTest(false, false);
+});
+
+add_task(async function test_defaultPrivateEngine_notifications() {
+ await defaultNotificationTest(true, true);
+});
+
+add_task(
+ async function test_defaultPrivateEngine_notifications_when_not_enabled() {
+ await Services.search.setDefault(
+ appDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ false
+ );
+
+ await defaultNotificationTest(true, true);
+ }
+);
+
+add_task(async function test_removeEngine() {
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await Services.search.setDefaultPrivate(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ const removedObserver = new SearchObserver([
+ SearchUtils.MODIFIED_TYPE.DEFAULT,
+ SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE,
+ SearchUtils.MODIFIED_TYPE.REMOVED,
+ ]);
+
+ await Services.search.removeEngine(engine);
+
+ await removedObserver;
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_opensearch.js b/toolkit/components/search/tests/xpcshell/test_opensearch.js
new file mode 100644
index 0000000000..bdd42860af
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_opensearch.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that OpenSearch engines are installed and set up correctly.
+ *
+ * Note: simple.xml, post.xml, suggestion.xml and suggestion-alternate.xml
+ * all use different namespaces to reflect the possibitities that may be
+ * installed.
+ * mozilla-ns.xml uses the mozilla namespace.
+ */
+
+"use strict";
+
+const tests = [
+ {
+ file: "simple.xml",
+ name: "simple",
+ description: "A small test engine",
+ searchForm: "https://example.com/",
+ searchUrl: "https://example.com/search?q=foo",
+ },
+ {
+ file: "post.xml",
+ name: "Post",
+ description: "",
+ // The POST method is not supported for `rel="searchform"` so we fallback
+ // to the `SearchForm` url.
+ searchForm: "http://engine-rel-searchform-post.xml/?search",
+ searchUrl: "https://example.com/post",
+ searchPostData: "searchterms=foo",
+ },
+ {
+ file: "suggestion.xml",
+ name: "suggestion",
+ description: "A small engine with suggestions",
+ queryCharset: "windows-1252",
+ searchForm: "http://engine-rel-searchform.xml/?search",
+ searchUrl: "https://example.com/search?q=foo",
+ suggestUrl: "https://example.com/suggest?suggestion=foo",
+ },
+ {
+ file: "suggestion-alternate.xml",
+ name: "suggestion-alternate",
+ description: "A small engine with suggestions",
+ searchForm: "https://example.com/",
+ searchUrl: "https://example.com/search?q=foo",
+ suggestUrl: "https://example.com/suggest?suggestion=foo",
+ },
+ {
+ file: "mozilla-ns.xml",
+ name: "mozilla-ns",
+ description: "An engine using mozilla namespace",
+ searchForm: "https://example.com/",
+ // mozilla-ns.xml also specifies a MozParam. However, they are only
+ // valid for app-provided engines, and hence the param should not show
+ // here.
+ searchUrl: "https://example.com/search?q=foo",
+ },
+];
+
+add_task(async function setup() {
+ Services.fog.initializeFOG();
+ useHttpServer("opensearch");
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+});
+
+for (const test of tests) {
+ add_task(async () => {
+ info(`Testing ${test.file}`);
+ let promiseEngineAdded = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.ADDED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ let engine = await Services.search.addOpenSearchEngine(
+ gDataUrl + test.file,
+ null
+ );
+ await promiseEngineAdded;
+ Assert.ok(engine, "Should have installed the engine.");
+
+ Assert.equal(engine.name, test.name, "Should have the correct name");
+ Assert.equal(
+ engine.description,
+ test.description,
+ "Should have a description"
+ );
+
+ Assert.equal(
+ engine.wrappedJSObject._loadPath,
+ `[http]localhost/${test.file}`
+ );
+
+ Assert.equal(
+ engine.queryCharset,
+ test.queryCharset ?? SearchUtils.DEFAULT_QUERY_CHARSET,
+ "Should have the expected query charset"
+ );
+
+ let submission = engine.getSubmission("foo");
+ Assert.equal(
+ submission.uri.spec,
+ test.searchUrl,
+ "Should have the correct search url"
+ );
+
+ if (test.searchPostData) {
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ sis.init(submission.postData);
+ let data = sis.read(submission.postData.available());
+ Assert.equal(
+ decodeURIComponent(data),
+ test.searchPostData,
+ "Should have received the correct POST data"
+ );
+ } else {
+ Assert.equal(
+ submission.postData,
+ null,
+ "Should have not received any POST data"
+ );
+ }
+
+ Assert.equal(
+ engine.searchForm,
+ test.searchForm,
+ "Should have the correct search form url"
+ );
+
+ submission = engine.getSubmission("foo", SearchUtils.URL_TYPE.SUGGEST_JSON);
+ if (test.suggestUrl) {
+ Assert.equal(
+ submission.uri.spec,
+ test.suggestUrl,
+ "Should have the correct suggest url"
+ );
+ } else {
+ Assert.equal(submission, null, "Should not have a suggestion url");
+ }
+ });
+}
+
+add_task(async function test_telemetry_reporting() {
+ // Use an engine from the previous tests.
+ let engine = Services.search.getEngineByName("simple");
+ Services.search.defaultEngine = engine;
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "other-simple",
+ displayName: "simple",
+ loadPath: "[http]localhost/simple.xml",
+ submissionUrl: "blank:",
+ verified: "verified",
+ },
+ });
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_opensearch_icon.js b/toolkit/components/search/tests/xpcshell/test_opensearch_icon.js
new file mode 100644
index 0000000000..937d1a58b9
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_opensearch_icon.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function setup() {
+ let server = useHttpServer();
+ server.registerContentType("sjs", "sjs");
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+});
+
+const tests = [
+ {
+ name: "Big Icon",
+ image: "bigIcon.ico",
+ expected: "data:image/png;base64,",
+ },
+ {
+ name: "Remote Icon",
+ image: "remoteIcon.ico",
+ expected: "data:image/x-icon;base64,",
+ },
+ {
+ name: "SVG Icon",
+ image: "svgIcon.svg",
+ expected: "data:image/svg+xml;base64,",
+ },
+];
+
+for (const test of tests) {
+ add_task(async function () {
+ info(`Testing ${test.name}`);
+
+ let promiseEngineAdded = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.ADDED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ let promiseEngineChanged = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ const engineData = {
+ baseURL: gDataUrl,
+ image: test.image,
+ name: test.name,
+ method: "GET",
+ };
+ // The easiest way to test adding the icon is via a generated xml, otherwise
+ // we have to somehow insert the address of the server into it.
+ SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engineMaker.sjs?${JSON.stringify(engineData)}`,
+ });
+ let engine = await promiseEngineAdded;
+ await promiseEngineChanged;
+
+ Assert.ok(engine.iconURI, "the engine has an icon");
+ Assert.ok(
+ engine.iconURI.spec.startsWith(test.expected),
+ "the icon is saved as an x-icon data url"
+ );
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_opensearch_icons_invalid.js b/toolkit/components/search/tests/xpcshell/test_opensearch_icons_invalid.js
new file mode 100644
index 0000000000..4f81b0e9d3
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_opensearch_icons_invalid.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that an installed engine can't use a resource URL for an icon */
+
+"use strict";
+
+add_task(async function setup() {
+ let server = useHttpServer("");
+ server.registerContentType("sjs", "sjs");
+ await AddonTestUtils.promiseStartupManager();
+});
+
+add_task(async function test_installedresourceicon() {
+ let engine1 = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}opensearch/resourceicon.xml`,
+ });
+ let engine2 = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}opensearch/chromeicon.xml`,
+ });
+
+ Assert.equal(null, engine1.iconURI);
+ Assert.equal(null, engine2.iconURI);
+});
+
+add_task(async function test_installedhttpplace() {
+ let observed = TestUtils.consoleMessageObserved(msg => {
+ return msg.wrappedJSObject.arguments[0].includes(
+ "Content type does not match expected"
+ );
+ });
+
+ // The easiest way to test adding the icon is via a generated xml, otherwise
+ // we have to somehow insert the address of the server into it.
+ let engine = await SearchTestUtils.promiseNewSearchEngine({
+ url:
+ `${gDataUrl}data/engineMaker.sjs?` +
+ JSON.stringify({
+ baseURL: gDataUrl,
+ image: "opensearch/resourceicon.xml",
+ name: "invalidicon",
+ method: "GET",
+ }),
+ });
+
+ await observed;
+
+ Assert.equal(null, engine.iconURI, "Should not have set an iconURI");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_opensearch_install_errors.js b/toolkit/components/search/tests/xpcshell/test_opensearch_install_errors.js
new file mode 100644
index 0000000000..62ae7d18ce
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_opensearch_install_errors.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test that various install failures are handled correctly.
+ */
+
+add_task(async function setup() {
+ useHttpServer("opensearch");
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+
+ // This test purposely attempts to load an invalid engine.
+ consoleAllowList.push("_onLoad: Failed to init engine!");
+ consoleAllowList.push("Invalid search plugin due to namespace not matching");
+});
+
+add_task(async function test_invalid_path_fails() {
+ await Assert.rejects(
+ Services.search.addOpenSearchEngine("http://invalid/data/engine.xml", null),
+ error => {
+ Assert.equal(
+ error.result,
+ Ci.nsISearchService.ERROR_DOWNLOAD_FAILURE,
+ "Should have returned download failure."
+ );
+ return true;
+ },
+ "Should fail to install an engine with an invalid path."
+ );
+});
+
+add_task(async function test_install_duplicate_fails() {
+ let engine = await Services.search.addOpenSearchEngine(
+ gDataUrl + "simple.xml",
+ null
+ );
+ Assert.equal(engine.name, "simple", "Should have installed the engine.");
+
+ await Assert.rejects(
+ Services.search.addOpenSearchEngine(gDataUrl + "simple.xml", null),
+ error => {
+ Assert.equal(
+ error.result,
+ Ci.nsISearchService.ERROR_DUPLICATE_ENGINE,
+ "Should have returned duplicate failure."
+ );
+ return true;
+ },
+ "Should fail to install a duplicate engine."
+ );
+});
+
+add_task(async function test_invalid_engine_from_dir() {
+ await Assert.rejects(
+ Services.search.addOpenSearchEngine(gDataUrl + "invalid.xml", null),
+ error => {
+ Assert.equal(
+ error.result,
+ Ci.nsISearchService.ERROR_ENGINE_CORRUPTED,
+ "Should have returned corruption failure."
+ );
+ return true;
+ },
+ "Should fail to install an invalid engine."
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_opensearch_telemetry.js b/toolkit/components/search/tests/xpcshell/test_opensearch_telemetry.js
new file mode 100644
index 0000000000..cd42a47371
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_opensearch_telemetry.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const { promiseStartupManager, promiseShutdownManager } = AddonTestUtils;
+
+const openSearchEngineFiles = [
+ "secure-and-securely-updated1.xml",
+ "secure-and-securely-updated2.xml",
+ "secure-and-securely-updated3.xml",
+ // An insecure search form should not affect telemetry.
+ "secure-and-securely-updated-insecure-form.xml",
+ "secure-and-insecurely-updated1.xml",
+ "secure-and-insecurely-updated2.xml",
+ "insecure-and-securely-updated1.xml",
+ "insecure-and-insecurely-updated1.xml",
+ "insecure-and-insecurely-updated2.xml",
+ "secure-and-no-update-url1.xml",
+ "insecure-and-no-update-url1.xml",
+ "secure-localhost.xml",
+ "secure-onionv2.xml",
+ "secure-onionv3.xml",
+];
+
+async function verifyTelemetry(probeNameFragment, engineCount, type) {
+ Services.telemetry.clearScalars();
+ await Services.search.runBackgroundChecks();
+
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent"),
+ `browser.searchinit.${probeNameFragment}`,
+ engineCount,
+ `Count of ${type} engines: ${engineCount}`
+ );
+}
+
+add_task(async function setup() {
+ useHttpServer("opensearch");
+
+ await promiseStartupManager();
+ await Services.search.init();
+
+ for (let file of openSearchEngineFiles) {
+ await Services.search.addOpenSearchEngine(gDataUrl + file, null);
+ }
+
+ registerCleanupFunction(async () => {
+ await promiseShutdownManager();
+ });
+});
+
+add_task(async function () {
+ verifyTelemetry("secure_opensearch_engine_count", 10, "secure");
+ verifyTelemetry("insecure_opensearch_engine_count", 4, "insecure");
+ verifyTelemetry("secure_opensearch_update_count", 5, "securely updated");
+ verifyTelemetry("insecure_opensearch_update_count", 4, "insecurely updated");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_opensearch_update.js b/toolkit/components/search/tests/xpcshell/test_opensearch_update.js
new file mode 100644
index 0000000000..5d67739b22
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_opensearch_update.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that user-set metadata isn't lost on engine update */
+
+"use strict";
+
+const KEYWORD = "keyword";
+let timerManager;
+
+add_task(async function setup() {
+ let server = useHttpServer("");
+ server.registerContentType("sjs", "sjs");
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+
+ timerManager = Cc["@mozilla.org/updates/timer-manager;1"].getService(
+ Ci.nsIUpdateTimerManager
+ );
+});
+
+add_task(async function test_installEngine_with_updates_disabled() {
+ const engineData = {
+ baseURL: gDataUrl,
+ name: "test engine",
+ method: "GET",
+ updateFile: "opensearch/simple.xml",
+ };
+
+ Services.prefs.setBoolPref(SearchUtils.BROWSER_SEARCH_PREF + "update", false);
+ Assert.ok(
+ !("search-engine-update-timer" in timerManager.wrappedJSObject._timers),
+ "Should not have registered the update timer already"
+ );
+
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}data/engineMaker.sjs?${JSON.stringify(engineData)}`,
+ });
+
+ Assert.ok(
+ Services.search.getEngineByName("test engine"),
+ "Should have added the test engine."
+ );
+ Assert.ok(
+ !("search-engine-update-timer" in timerManager.wrappedJSObject._timers),
+ "Should have not registered the update timer when updates are disabled"
+ );
+});
+
+add_task(async function test_installEngine_with_updates_enabled() {
+ const engineData = {
+ baseURL: gDataUrl,
+ name: "original engine",
+ method: "GET",
+ updateFile: "opensearch/simple.xml",
+ };
+
+ Services.prefs.setBoolPref(SearchUtils.BROWSER_SEARCH_PREF + "update", true);
+
+ Assert.ok(
+ !("search-engine-update-timer" in timerManager.wrappedJSObject._timers),
+ "Should not have registered the update timer already"
+ );
+
+ let engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}data/engineMaker.sjs?${JSON.stringify(engineData)}`,
+ });
+
+ Assert.ok(
+ "search-engine-update-timer" in timerManager.wrappedJSObject._timers,
+ "Should have registered the update timer"
+ );
+
+ engine.alias = KEYWORD;
+ await Services.search.moveEngine(engine, 0);
+
+ Assert.ok(
+ !!Services.search.getEngineByName("original engine"),
+ "Should be able to get the engine by the original name"
+ );
+ Assert.ok(
+ !Services.search.getEngineByName("simple"),
+ "Should not be able to get the engine by the new name"
+ );
+});
+
+add_task(async function test_engineUpdate() {
+ const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
+
+ let promiseUpdate = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+
+ // set last update to 8 days ago, since the default interval is 7, then
+ // trigger an update
+ let engine = Services.search.getEngineByName("original engine");
+ engine.wrappedJSObject.setAttr("updateexpir", Date.now() - ONE_DAY_IN_MS * 8);
+ Services.search.QueryInterface(Ci.nsITimerCallback).notify(null);
+
+ await promiseUpdate;
+
+ Assert.equal(engine.name, "simple", "Should have updated the engine's name");
+
+ Assert.equal(engine.alias, KEYWORD, "Should have kept the keyword");
+ Assert.equal(
+ engine.wrappedJSObject.getAttr("order"),
+ 1,
+ "Should have kept the order"
+ );
+
+ Assert.ok(
+ !!Services.search.getEngineByName("simple"),
+ "Should be able to get the engine by the new name"
+ );
+ Assert.ok(
+ !Services.search.getEngineByName("original engine"),
+ "Should not be able to get the engine by the old name"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_override_allowlist.js b/toolkit/components/search/tests/xpcshell/test_override_allowlist.js
new file mode 100644
index 0000000000..24a2ecb8e6
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_override_allowlist.js
@@ -0,0 +1,391 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const kBaseURL = "https://example.com/";
+const kSearchEngineURL = `${kBaseURL}?q={searchTerms}&foo=myparams`;
+const kOverriddenEngineName = "Simple Engine";
+
+const allowlist = [
+ {
+ thirdPartyId: "test@thirdparty.example.com",
+ overridesId: "simple@search.mozilla.org",
+ urls: [],
+ },
+];
+
+const tests = [
+ {
+ title: "test_not_changing_anything",
+ startupReason: "ADDON_INSTALL",
+ search_provider: {
+ is_default: true,
+ name: "MozParamsTest2",
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL,
+ },
+ expected: {
+ switchToDefaultAllowed: false,
+ canInstallEngine: true,
+ overridesEngine: false,
+ },
+ },
+ {
+ title: "test_changing_default_engine",
+ startupReason: "ADDON_INSTALL",
+ search_provider: {
+ is_default: true,
+ name: kOverriddenEngineName,
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL,
+ },
+ expected: {
+ switchToDefaultAllowed: true,
+ canInstallEngine: false,
+ overridesEngine: false,
+ },
+ },
+ {
+ title: "test_changing_default_engine",
+ startupReason: "ADDON_ENABLE",
+ search_provider: {
+ is_default: true,
+ name: kOverriddenEngineName,
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL,
+ },
+ expected: {
+ switchToDefaultAllowed: true,
+ canInstallEngine: false,
+ overridesEngine: false,
+ },
+ },
+ {
+ title: "test_overriding_default_engine",
+ startupReason: "ADDON_INSTALL",
+ search_provider: {
+ is_default: true,
+ name: kOverriddenEngineName,
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL,
+ },
+ allowlistUrls: [
+ {
+ search_url: kSearchEngineURL,
+ },
+ ],
+ expected: {
+ switchToDefaultAllowed: true,
+ canInstallEngine: false,
+ overridesEngine: true,
+ searchUrl: kSearchEngineURL,
+ },
+ },
+ {
+ title: "test_overriding_default_engine_enable",
+ startupReason: "ADDON_ENABLE",
+ search_provider: {
+ is_default: true,
+ name: kOverriddenEngineName,
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL,
+ },
+ allowlistUrls: [
+ {
+ search_url: kSearchEngineURL,
+ },
+ ],
+ expected: {
+ switchToDefaultAllowed: true,
+ canInstallEngine: false,
+ overridesEngine: true,
+ searchUrl: kSearchEngineURL,
+ },
+ },
+ {
+ title: "test_overriding_default_engine_different_url",
+ startupReason: "ADDON_INSTALL",
+ search_provider: {
+ is_default: true,
+ name: kOverriddenEngineName,
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL + "a",
+ },
+ allowlistUrls: [
+ {
+ search_url: kSearchEngineURL,
+ },
+ ],
+ expected: {
+ switchToDefaultAllowed: true,
+ canInstallEngine: false,
+ overridesEngine: false,
+ },
+ },
+ {
+ title: "test_overriding_default_engine_get_params",
+ startupReason: "ADDON_INSTALL",
+ search_provider: {
+ is_default: true,
+ name: kOverriddenEngineName,
+ keyword: "MozSearch",
+ search_url: kBaseURL,
+ search_url_get_params: "q={searchTerms}&enc=UTF-8",
+ },
+ allowlistUrls: [
+ {
+ search_url: kBaseURL,
+ search_url_get_params: "q={searchTerms}&enc=UTF-8",
+ },
+ ],
+ expected: {
+ switchToDefaultAllowed: true,
+ canInstallEngine: false,
+ overridesEngine: true,
+ searchUrl: `${kBaseURL}?q={searchTerms}&enc=UTF-8`,
+ },
+ },
+ {
+ title: "test_overriding_default_engine_different_get_params",
+ startupReason: "ADDON_INSTALL",
+ search_provider: {
+ is_default: true,
+ name: kOverriddenEngineName,
+ keyword: "MozSearch",
+ search_url: kBaseURL,
+ search_url_get_params: "q={searchTerms}&enc=UTF-8a",
+ },
+ allowlistUrls: [
+ {
+ search_url: kBaseURL,
+ search_url_get_params: "q={searchTerms}&enc=UTF-8",
+ },
+ ],
+ expected: {
+ switchToDefaultAllowed: true,
+ canInstallEngine: false,
+ overridesEngine: false,
+ },
+ },
+ {
+ title: "test_overriding_default_engine_post_params",
+ startupReason: "ADDON_INSTALL",
+ search_provider: {
+ is_default: true,
+ name: kOverriddenEngineName,
+ keyword: "MozSearch",
+ search_url: kBaseURL,
+ search_url_post_params: "q={searchTerms}&enc=UTF-8",
+ },
+ allowlistUrls: [
+ {
+ search_url: kBaseURL,
+ search_url_post_params: "q={searchTerms}&enc=UTF-8",
+ },
+ ],
+ expected: {
+ switchToDefaultAllowed: true,
+ canInstallEngine: false,
+ overridesEngine: true,
+ searchUrl: `${kBaseURL}`,
+ postData: "q={searchTerms}&enc=UTF-8",
+ },
+ },
+ {
+ title: "test_overriding_default_engine_different_post_params",
+ startupReason: "ADDON_INSTALL",
+ search_provider: {
+ is_default: true,
+ name: kOverriddenEngineName,
+ keyword: "MozSearch",
+ search_url: kBaseURL,
+ search_url_post_params: "q={searchTerms}&enc=UTF-8a",
+ },
+ allowlistUrls: [
+ {
+ search_url: kBaseURL,
+ search_url_post_params: "q={searchTerms}&enc=UTF-8",
+ },
+ ],
+ expected: {
+ switchToDefaultAllowed: true,
+ canInstallEngine: false,
+ overridesEngine: false,
+ },
+ },
+ {
+ title: "test_overriding_default_engine_search_form",
+ startupReason: "ADDON_INSTALL",
+ search_provider: {
+ is_default: true,
+ name: kOverriddenEngineName,
+ keyword: "MozSearch",
+ search_url: kBaseURL,
+ search_form: "https://example.com/form",
+ },
+ allowlistUrls: [
+ {
+ search_url: kBaseURL,
+ search_form: "https://example.com/form",
+ },
+ ],
+ expected: {
+ switchToDefaultAllowed: true,
+ canInstallEngine: false,
+ overridesEngine: true,
+ searchUrl: `${kBaseURL}`,
+ searchForm: "https://example.com/form",
+ },
+ },
+ {
+ title: "test_overriding_default_engine_different_search_form",
+ startupReason: "ADDON_INSTALL",
+ search_provider: {
+ is_default: true,
+ name: kOverriddenEngineName,
+ keyword: "MozSearch",
+ search_url: kBaseURL,
+ search_form: "https://example.com/forma",
+ },
+ allowlistUrls: [
+ {
+ search_url: kBaseURL,
+ search_form: "https://example.com/form",
+ },
+ ],
+ expected: {
+ switchToDefaultAllowed: true,
+ canInstallEngine: false,
+ overridesEngine: false,
+ },
+ },
+];
+
+let baseExtension;
+let remoteSettingsStub;
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("simple-engines");
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+
+ baseExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "test@thirdparty.example.com",
+ },
+ },
+ },
+ useAddonManager: "permanent",
+ });
+ await baseExtension.startup();
+
+ const settings = await RemoteSettings(SearchUtils.SETTINGS_ALLOWLIST_KEY);
+ remoteSettingsStub = sinon.stub(settings, "get").returns([]);
+
+ registerCleanupFunction(async () => {
+ await baseExtension.unload();
+ });
+});
+
+for (const test of tests) {
+ add_task(async () => {
+ info(test.title);
+
+ let extension = {
+ ...baseExtension,
+ startupReason: test.startupReason,
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: test.search_provider,
+ },
+ },
+ };
+
+ if (test.expected.overridesEngine) {
+ remoteSettingsStub.returns([
+ { ...allowlist[0], urls: test.allowlistUrls },
+ ]);
+ }
+
+ let result = await Services.search.maybeSetAndOverrideDefault(extension);
+ Assert.equal(
+ result.canChangeToAppProvided,
+ test.expected.switchToDefaultAllowed,
+ "Should have returned the correct value for allowing switch to default or not."
+ );
+ Assert.equal(
+ result.canInstallEngine,
+ test.expected.canInstallEngine,
+ "Should have returned the correct value for allowing to install the engine or not."
+ );
+
+ let engine = await Services.search.getEngineByName(kOverriddenEngineName);
+ Assert.equal(
+ !!engine.wrappedJSObject.getAttr("overriddenBy"),
+ test.expected.overridesEngine,
+ "Should have correctly overridden or not."
+ );
+
+ Assert.equal(
+ engine.telemetryId,
+ "simple" + (test.expected.overridesEngine ? "-addon" : ""),
+ "Should set the correct telemetry Id"
+ );
+
+ if (test.expected.overridesEngine) {
+ let submission = engine.getSubmission("{searchTerms}");
+ Assert.equal(
+ decodeURI(submission.uri.spec),
+ test.expected.searchUrl,
+ "Should have set the correct url on an overriden engine"
+ );
+
+ if (test.expected.search_form) {
+ Assert.equal(
+ engine.wrappedJSObject._searchForm,
+ test.expected.searchForm,
+ "Should have overridden the search form."
+ );
+ }
+
+ if (test.expected.postData) {
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ sis.init(submission.postData);
+ let data = sis.read(submission.postData.available());
+ Assert.equal(
+ decodeURIComponent(data),
+ test.expected.postData,
+ "Should have overridden the postData"
+ );
+ }
+
+ // As we're not testing the WebExtension manager as well,
+ // set this engine as default so we can check the telemetry data.
+ let oldDefaultEngine = Services.search.defaultEngine;
+ Services.search.defaultEngine = engine;
+
+ let engineInfo = Services.search.getDefaultEngineInfo();
+ Assert.deepEqual(
+ engineInfo,
+ {
+ defaultSearchEngine: "simple-addon",
+ defaultSearchEngineData: {
+ loadPath: "[addon]simple@search.mozilla.org",
+ name: "Simple Engine",
+ origin: "default",
+ submissionURL: test.expected.searchUrl.replace("{searchTerms}", ""),
+ },
+ },
+ "Should return the extended identifier and alternate submission url to telemetry"
+ );
+ Services.search.defaultEngine = oldDefaultEngine;
+
+ engine.wrappedJSObject.removeExtensionOverride();
+ }
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js b/toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js
new file mode 100644
index 0000000000..bee90dbb5b
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js
@@ -0,0 +1,182 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests getAlternateDomains API.
+ */
+
+"use strict";
+
+add_task(async function setup() {
+ useHttpServer();
+ await AddonTestUtils.promiseStartupManager();
+});
+
+add_task(async function test_parseSubmissionURL() {
+ let engine1 = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engine.xml`,
+ });
+ let engine2 = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engine-fr.xml`,
+ });
+
+ await SearchTestUtils.installSearchExtension({
+ name: "bacon_addParam",
+ keyword: "bacon_addParam",
+ encoding: "windows-1252",
+ search_url: "https://www.bacon.test/find",
+ });
+ await SearchTestUtils.installSearchExtension({
+ name: "idn_addParam",
+ keyword: "idn_addParam",
+ search_url: "https://www.xn--bcher-kva.ch/search",
+ });
+ let engine3 = Services.search.getEngineByName("bacon_addParam");
+ let engine4 = Services.search.getEngineByName("idn_addParam");
+
+ // The following engine provides it's query keyword in
+ // its template in the form of q={searchTerms}
+ let engine5 = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engine2.xml`,
+ });
+
+ // The following engines cannot identify the search parameter.
+ await SearchTestUtils.installSearchExtension({
+ name: "bacon",
+ keyword: "bacon",
+ search_url: "https://www.bacon.moz/search?q=",
+ search_url_get_params: "",
+ });
+
+ await Services.search.setDefault(
+ engine1,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ // Hide the default engines to prevent them from being used in the search.
+ for (let engine of await Services.search.getAppProvidedEngines()) {
+ await Services.search.removeEngine(engine);
+ }
+
+ // Test the first engine, whose URLs use UTF-8 encoding.
+ // This also tests the query parameter in a different position not being the
+ // first parameter.
+ let url = "https://www.google.com/search?foo=bar&q=caff%C3%A8";
+ let result = Services.search.parseSubmissionURL(url);
+ Assert.equal(result.engine.wrappedJSObject, engine1);
+ Assert.equal(result.terms, "caff\u00E8");
+
+ // The second engine uses a locale-specific domain that is an alternate domain
+ // of the first one, but the second engine should get priority when matching.
+ // The URL used with this engine uses ISO-8859-1 encoding instead.
+ url = "https://www.google.fr/search?q=caff%E8";
+ result = Services.search.parseSubmissionURL(url);
+ Assert.equal(result.engine.wrappedJSObject, engine2);
+ Assert.equal(result.terms, "caff\u00E8");
+
+ // Test a domain that is an alternate domain of those defined. In this case,
+ // the first matching engine from the ordered list should be returned.
+ url = "https://www.google.co.uk/search?q=caff%C3%A8";
+ result = Services.search.parseSubmissionURL(url);
+ Assert.equal(result.engine.wrappedJSObject, engine1);
+ Assert.equal(result.terms, "caff\u00E8");
+
+ // We support parsing URLs from a dynamically added engine.
+ url = "https://www.bacon.test/find?q=caff%E8";
+ result = Services.search.parseSubmissionURL(url);
+ Assert.equal(result.engine, engine3);
+ Assert.equal(result.terms, "caff\u00E8");
+
+ // Test URLs with unescaped unicode characters.
+ url = "https://www.google.com/search?q=foo+b\u00E4r";
+ result = Services.search.parseSubmissionURL(url);
+ Assert.equal(result.engine.wrappedJSObject, engine1);
+ Assert.equal(result.terms, "foo b\u00E4r");
+
+ // Test search engines with unescaped IDNs.
+ url = "https://www.b\u00FCcher.ch/search?q=foo+bar";
+ result = Services.search.parseSubmissionURL(url);
+ Assert.equal(result.engine, engine4);
+ Assert.equal(result.terms, "foo bar");
+
+ // Test search engines with escaped IDNs.
+ url = "https://www.xn--bcher-kva.ch/search?q=foo+bar";
+ result = Services.search.parseSubmissionURL(url);
+ Assert.equal(result.engine, engine4);
+ Assert.equal(result.terms, "foo bar");
+
+ // Parsing of parameters from an engine template URL is not supported
+ // if no matching parameter value template is provided.
+ Assert.equal(
+ Services.search.parseSubmissionURL("https://www.bacon.moz/search?q=")
+ .engine,
+ null
+ );
+
+ // Parsing of parameters from an engine template URL is supported
+ // if a matching parameter value template is provided.
+ url = "https://duckduckgo.com/?foo=bar&q=caff%C3%A8";
+ result = Services.search.parseSubmissionURL(url);
+ Assert.equal(result.engine.wrappedJSObject, engine5);
+ Assert.equal(result.terms, "caff\u00E8");
+
+ // If the search params are in the template, the query parameter
+ // doesn't need to be separated from the host by a slash, only by
+ // by a question mark.
+ url = "https://duckduckgo.com?foo=bar&q=caff%C3%A8";
+ result = Services.search.parseSubmissionURL(url);
+ Assert.equal(result.engine.wrappedJSObject, engine5);
+ Assert.equal(result.terms, "caff\u00E8");
+
+ // HTTP and HTTPS schemes are interchangeable.
+ url = "https://www.google.com/search?q=caff%C3%A8";
+ result = Services.search.parseSubmissionURL(url);
+ Assert.equal(result.engine.wrappedJSObject, engine1);
+ Assert.equal(result.terms, "caff\u00E8");
+
+ // Decoding search terms with multiple spaces should work.
+ result = Services.search.parseSubmissionURL(
+ "https://www.google.com/search?q=+with++spaces+"
+ );
+ Assert.equal(result.engine.wrappedJSObject, engine1);
+ Assert.equal(result.terms, " with spaces ");
+
+ // Parsing search terms with ampersands should work.
+ result = Services.search.parseSubmissionURL(
+ "https://www.google.com/search?q=with%26ampersand"
+ );
+ Assert.equal(result.engine.wrappedJSObject, engine1);
+ Assert.equal(result.terms, "with&ampersand");
+
+ // Capitals in the path should work
+ result = Services.search.parseSubmissionURL(
+ "https://www.google.com/SEARCH?q=caps"
+ );
+ Assert.equal(result.engine.wrappedJSObject, engine1);
+ Assert.equal(result.terms, "caps");
+
+ // An empty query parameter should work the same.
+ url = "https://www.google.com/search?q=";
+ result = Services.search.parseSubmissionURL(url);
+ Assert.equal(result.engine.wrappedJSObject, engine1);
+ Assert.equal(result.terms, "");
+
+ // There should be no match when the path is different.
+ result = Services.search.parseSubmissionURL(
+ "https://www.google.com/search/?q=test"
+ );
+ Assert.equal(result.engine, null);
+ Assert.equal(result.terms, "");
+
+ // There should be no match when the argument is different.
+ result = Services.search.parseSubmissionURL(
+ "https://www.google.com/search?q2=test"
+ );
+ Assert.equal(result.engine, null);
+ Assert.equal(result.terms, "");
+
+ // There should be no match for URIs that are not HTTP or HTTPS.
+ result = Services.search.parseSubmissionURL("file://localhost/search?q=test");
+ Assert.equal(result.engine, null);
+ Assert.equal(result.terms, "");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_policyEngine.js b/toolkit/components/search/tests/xpcshell/test_policyEngine.js
new file mode 100644
index 0000000000..4dd58c8fc7
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_policyEngine.js
@@ -0,0 +1,185 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that Enterprise Policy Engines can be installed correctly.
+ */
+
+"use strict";
+
+const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+);
+
+SearchSettings.SETTINGS_INVALIDATION_DELAY = 100;
+
+/**
+ * Loads a new enterprise policy, and re-initialise the search service
+ * with the new policy. Also waits for the search service to write the settings
+ * file to disk.
+ *
+ * @param {object} policy
+ * The enterprise policy to use.
+ */
+async function setupPolicyEngineWithJson(policy) {
+ Services.search.wrappedJSObject.reset();
+
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson(policy);
+
+ let settingsWritten = SearchTestUtils.promiseSearchNotification(
+ "write-settings-to-disk-complete"
+ );
+ await Services.search.init();
+ await settingsWritten;
+}
+
+add_task(async function setup() {
+ // This initializes the policy engine for xpcshell tests
+ let policies = Cc["@mozilla.org/enterprisepolicies;1"].getService(
+ Ci.nsIObserver
+ );
+ policies.observe(null, "policies-startup", null);
+
+ Services.fog.initializeFOG();
+ await AddonTestUtils.promiseStartupManager();
+ await SearchTestUtils.useTestEngines();
+
+ SearchUtils.GENERAL_SEARCH_ENGINE_IDS = new Set([
+ "engine-resourceicon@search.mozilla.org",
+ ]);
+});
+
+add_task(async function test_enterprise_policy_engine() {
+ await setupPolicyEngineWithJson({
+ policies: {
+ SearchEngines: {
+ Add: [
+ {
+ Name: "policy",
+ Description: "Test policy engine",
+ IconURL: "data:image/gif;base64,R0lGODl",
+ Alias: "p",
+ URLTemplate: "https://example.com?q={searchTerms}",
+ SuggestURLTemplate: "https://example.com/suggest/?q={searchTerms}",
+ },
+ ],
+ },
+ },
+ });
+
+ let engine = Services.search.getEngineByName("policy");
+ Assert.ok(engine, "Should have installed the engine.");
+
+ Assert.equal(engine.name, "policy", "Should have the correct name");
+ Assert.equal(
+ engine.description,
+ "Test policy engine",
+ "Should have a description"
+ );
+ Assert.deepEqual(engine.aliases, ["p"], "Should have the correct alias");
+
+ let submission = engine.getSubmission("foo");
+ Assert.equal(
+ submission.uri.spec,
+ "https://example.com/?q=foo",
+ "Should have the correct search url"
+ );
+
+ submission = engine.getSubmission("foo", SearchUtils.URL_TYPE.SUGGEST_JSON);
+ Assert.equal(
+ submission.uri.spec,
+ "https://example.com/suggest/?q=foo",
+ "Should have the correct suggest url"
+ );
+
+ Services.search.defaultEngine = engine;
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "other-policy",
+ displayName: "policy",
+ loadPath: "[policy]",
+ submissionUrl: "blank:",
+ verified: "verified",
+ },
+ });
+});
+
+add_task(async function test_enterprise_policy_engine_hidden_persisted() {
+ // Set the engine alias, and wait for the settings to be written.
+ let settingsWritten = SearchTestUtils.promiseSearchNotification(
+ "write-settings-to-disk-complete"
+ );
+ let engine = Services.search.getEngineByName("policy");
+ engine.hidden = "p1";
+ engine.alias = "p1";
+ await settingsWritten;
+
+ // This will reset and re-initialise the search service.
+ await setupPolicyEngineWithJson({
+ policies: {
+ SearchEngines: {
+ Add: [
+ {
+ Name: "policy",
+ Description: "Test policy engine",
+ IconURL: "data:image/gif;base64,R0lGODl",
+ Alias: "p",
+ URLTemplate: "https://example.com?q={searchTerms}",
+ SuggestURLTemplate: "https://example.com/suggest/?q={searchTerms}",
+ },
+ ],
+ },
+ },
+ });
+
+ engine = Services.search.getEngineByName("policy");
+ Assert.equal(engine.alias, "p1", "Should have retained the engine alias");
+ Assert.ok(engine.hidden, "Should have kept the engine hidden");
+});
+
+add_task(async function test_enterprise_policy_engine_remove() {
+ // This will reset and re-initialise the search service.
+ await setupPolicyEngineWithJson({
+ policies: {},
+ });
+
+ Assert.ok(
+ !Services.search.getEngineByName("policy"),
+ "Should not have the policy engine installed"
+ );
+
+ let settings = await promiseSettingsData();
+ Assert.ok(
+ !settings.engines.find(e => e.name == "p1"),
+ "Should not have the engine settings stored"
+ );
+});
+
+add_task(async function test_enterprise_policy_hidden_default() {
+ await setupPolicyEngineWithJson({
+ policies: {
+ SearchEngines: {
+ Remove: ["Test search engine"],
+ },
+ },
+ });
+
+ Services.search.resetToAppDefaultEngine();
+
+ Assert.equal(Services.search.defaultEngine.name, "engine-resourceicon");
+});
+
+add_task(async function test_enterprise_policy_default() {
+ await setupPolicyEngineWithJson({
+ policies: {
+ SearchEngines: {
+ Default: "engine-pref",
+ },
+ },
+ });
+
+ Services.search.resetToAppDefaultEngine();
+
+ Assert.equal(Services.search.defaultEngine.name, "engine-pref");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_pref.js b/toolkit/components/search/tests/xpcshell/test_pref.js
new file mode 100644
index 0000000000..2445ea2100
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_pref.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that MozParam condition="pref" values used in search URLs are from the
+ * default branch, and that their special characters are URL encoded. */
+
+"use strict";
+
+const defaultBranch = Services.prefs.getDefaultBranch(
+ SearchUtils.BROWSER_SEARCH_PREF
+);
+const baseURL = "https://www.google.com/search?q=foo";
+
+add_task(async function setup() {
+ // The test engines used in this test need to be recognized as 'default'
+ // engines, or their MozParams will be ignored.
+ await SearchTestUtils.useTestEngines();
+});
+
+add_task(async function test_pref_initial_value() {
+ defaultBranch.setCharPref("param.code", "good&id=unique");
+ Services.prefs.setCharPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "param.code",
+ "bad"
+ );
+
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+
+ const engine = Services.search.getEngineByName("engine-pref");
+ const base = baseURL + "&code=";
+ Assert.equal(
+ engine.getSubmission("foo").uri.spec,
+ base + "good%26id%3Dunique",
+ "Should have got the submission URL with the correct code"
+ );
+
+ // Now clear the user-set preference. Having a user set preference means
+ // we don't get updates from the pref service of changes on the default
+ // branch. Normally, this won't be an issue, since we don't expect users
+ // to be playing with these prefs, and worst-case, they'll just get the
+ // actual change on restart.
+ Services.prefs.clearUserPref(SearchUtils.BROWSER_SEARCH_PREF + "param.code");
+});
+
+add_task(async function test_pref_updated() {
+ // Update the pref without re-init nor restart.
+ defaultBranch.setCharPref("param.code", "supergood&id=unique123456");
+
+ const engine = Services.search.getEngineByName("engine-pref");
+ const base = baseURL + "&code=";
+ Assert.equal(
+ engine.getSubmission("foo").uri.spec,
+ base + "supergood%26id%3Dunique123456",
+ "Should have got the submission URL with the updated code"
+ );
+});
+
+add_task(async function test_pref_cleared() {
+ // Update the pref without re-init nor restart.
+ // Note you can't delete a preference from the default branch.
+ defaultBranch.setCharPref("param.code", "");
+
+ let engine = Services.search.getEngineByName("engine-pref");
+ Assert.equal(
+ engine.getSubmission("foo").uri.spec,
+ baseURL,
+ "Should have just the base URL after the pref was cleared"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_purpose.js b/toolkit/components/search/tests/xpcshell/test_purpose.js
new file mode 100644
index 0000000000..7320276e4f
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_purpose.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test that a search purpose can be specified and that query parameters for
+ * that purpose are included in the search URL.
+ */
+
+"use strict";
+
+add_task(async function setup() {
+ // The test engines used in this test need to be recognized as 'default'
+ // engines, or their MozParams used to set the purpose will be ignored.
+ await SearchTestUtils.useTestEngines();
+
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+});
+
+add_task(async function test_purpose() {
+ let engine = Services.search.getEngineByName("Test search engine");
+
+ function check_submission(aValue, aSearchTerm, aType, aPurpose) {
+ let submissionURL = engine.getSubmission(aSearchTerm, aType, aPurpose).uri
+ .spec;
+ let searchParams = new URLSearchParams(submissionURL.split("?")[1]);
+ if (aValue) {
+ Assert.equal(searchParams.get("channel"), aValue);
+ } else {
+ Assert.ok(!searchParams.has("channel"));
+ }
+ Assert.equal(searchParams.get("q"), aSearchTerm);
+ }
+
+ check_submission("", "foo");
+ check_submission("", "foo", null);
+ check_submission("", "foo", "text/html");
+ check_submission("rcs", "foo", null, "contextmenu");
+ check_submission("rcs", "foo", "text/html", "contextmenu");
+ check_submission("fflb", "foo", null, "keyword");
+ check_submission("fflb", "foo", "text/html", "keyword");
+ check_submission("", "foo", "text/html", "invalid");
+
+ // Tests for a purpose on the search form (ie. empty query).
+ engine = Services.search.getEngineByName("engine-rel-searchform-purpose");
+
+ // See bug 1485508
+ Assert.ok(!engine.searchForm.includes("?&"));
+
+ // verify that the 'system' purpose falls back to the 'searchbar' purpose.
+ check_submission("sb", "foo", "text/html", "system");
+ check_submission("sb", "foo", "text/html", "searchbar");
+});
+
+add_task(async function test_purpose() {
+ let engine = Services.search.getEngineByName(
+ "Test search engine (Reordered)"
+ );
+
+ function check_submission(aValue, aSearchTerm, aType, aPurpose) {
+ let submissionURL = engine.getSubmission(aSearchTerm, aType, aPurpose).uri
+ .spec;
+ let searchParams = new URLSearchParams(submissionURL.split("?")[1]);
+ if (aValue) {
+ Assert.equal(searchParams.get("channel"), aValue);
+ } else {
+ Assert.ok(!searchParams.has("channel"));
+ }
+ Assert.equal(searchParams.get("q"), aSearchTerm);
+ }
+
+ check_submission("", "foo");
+ check_submission("", "foo", null);
+ check_submission("", "foo", "text/html");
+ check_submission("rcs", "foo", null, "contextmenu");
+ check_submission("rcs", "foo", "text/html", "contextmenu");
+ check_submission("fflb", "foo", null, "keyword");
+ check_submission("fflb", "foo", "text/html", "keyword");
+ check_submission("", "foo", "text/html", "invalid");
+
+ // See bug 1485508
+ Assert.ok(!engine.searchForm.includes("?&"));
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_reload_engines.js b/toolkit/components/search/tests/xpcshell/test_reload_engines.js
new file mode 100644
index 0000000000..4d3a3d3659
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_reload_engines.js
@@ -0,0 +1,316 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const CONFIG = [
+ {
+ // Engine initially default, but the defaults will be changed to engine-pref.
+ webExtension: {
+ id: "engine@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ default: "yes",
+ defaultPrivate: "yes",
+ },
+ {
+ included: { regions: ["FR"] },
+ default: "no",
+ defaultPrivate: "no",
+ },
+ ],
+ },
+ {
+ // This will become defaults when region is changed to FR.
+ webExtension: {
+ id: "engine-pref@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ {
+ included: { regions: ["FR"] },
+ default: "yes",
+ defaultPrivate: "yes",
+ },
+ ],
+ },
+ {
+ // This engine will get an update when region is changed to FR.
+ webExtension: {
+ id: "engine-chromeicon@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ {
+ included: { regions: ["FR"] },
+ extraParams: [
+ { name: "c", value: "my-test" },
+ { name: "q1", value: "{searchTerms}" },
+ ],
+ },
+ ],
+ },
+ {
+ // This engine will be removed when the region is changed to FR.
+ webExtension: {
+ id: "engine-rel-searchform-purpose@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ excluded: { regions: ["FR"] },
+ },
+ ],
+ },
+ {
+ // This engine will be added when the region is changed to FR.
+ webExtension: {
+ id: "engine-reordered@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { regions: ["FR"] },
+ },
+ ],
+ },
+ {
+ // This engine will be re-ordered and have a changed name, when moved to FR.
+ webExtension: {
+ id: "engine-resourceicon@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ excluded: { regions: ["FR"] },
+ },
+ {
+ included: { regions: ["FR"] },
+ webExtension: {
+ locales: ["gd"],
+ },
+ orderHint: 30,
+ },
+ ],
+ },
+ {
+ // This engine has the same name, but still should be replaced correctly.
+ webExtension: {
+ id: "engine-same-name@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ excluded: { regions: ["FR"] },
+ },
+ {
+ included: { regions: ["FR"] },
+ webExtension: {
+ locales: ["gd"],
+ },
+ },
+ ],
+ },
+];
+
+async function visibleEngines() {
+ return (await Services.search.getVisibleEngines()).map(e => e.identifier);
+}
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref("browser.search.separatePrivateDefault", true);
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ true
+ );
+
+ SearchTestUtils.useMockIdleService();
+ await SearchTestUtils.useTestEngines("data", null, CONFIG);
+ await AddonTestUtils.promiseStartupManager();
+});
+
+// This is to verify that the loaded configuration matches what we expect for
+// the test.
+add_task(async function test_initial_config_correct() {
+ Region._setHomeRegion("", false);
+
+ await Services.search.init();
+
+ const installedEngines = await Services.search.getAppProvidedEngines();
+ Assert.deepEqual(
+ installedEngines.map(e => e.identifier),
+ [
+ "engine",
+ "engine-chromeicon",
+ "engine-pref",
+ "engine-rel-searchform-purpose",
+ "engine-resourceicon",
+ "engine-same-name",
+ ],
+ "Should have the correct list of engines installed."
+ );
+
+ Assert.equal(
+ (await Services.search.getDefault()).identifier,
+ "engine",
+ "Should have loaded the expected default engine"
+ );
+
+ Assert.equal(
+ (await Services.search.getDefaultPrivate()).identifier,
+ "engine",
+ "Should have loaded the expected private default engine"
+ );
+});
+
+add_task(async function test_config_updated_engine_changes() {
+ // Update the config.
+ const reloadObserved =
+ SearchTestUtils.promiseSearchNotification("engines-reloaded");
+ const defaultEngineChanged = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.DEFAULT,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ const defaultPrivateEngineChanged = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+
+ const enginesAdded = [];
+ const enginesModified = [];
+ const enginesRemoved = [];
+
+ function enginesObs(subject, topic, data) {
+ if (data == SearchUtils.MODIFIED_TYPE.ADDED) {
+ enginesAdded.push(subject.QueryInterface(Ci.nsISearchEngine).identifier);
+ } else if (data == SearchUtils.MODIFIED_TYPE.CHANGED) {
+ enginesModified.push(
+ subject.QueryInterface(Ci.nsISearchEngine).identifier
+ );
+ } else if (data == SearchUtils.MODIFIED_TYPE.REMOVED) {
+ enginesRemoved.push(subject.QueryInterface(Ci.nsISearchEngine).name);
+ }
+ }
+ Services.obs.addObserver(enginesObs, SearchUtils.TOPIC_ENGINE_MODIFIED);
+
+ Region._setHomeRegion("FR", false);
+
+ await Services.search.wrappedJSObject._maybeReloadEngines();
+
+ await reloadObserved;
+ Services.obs.removeObserver(enginesObs, SearchUtils.TOPIC_ENGINE_MODIFIED);
+
+ Assert.deepEqual(
+ enginesAdded,
+ ["engine-resourceicon-gd", "engine-reordered"],
+ "Should have added the correct engines"
+ );
+
+ Assert.deepEqual(
+ enginesModified.sort(),
+ ["engine", "engine-chromeicon", "engine-pref", "engine-same-name-gd"],
+ "Should have modified the expected engines"
+ );
+
+ Assert.deepEqual(
+ enginesRemoved,
+ ["engine-rel-searchform-purpose", "engine-resourceicon"],
+ "Should have removed the expected engine"
+ );
+
+ const installedEngines = await Services.search.getAppProvidedEngines();
+
+ Assert.deepEqual(
+ installedEngines.map(e => e.identifier),
+ [
+ "engine-pref",
+ "engine-resourceicon-gd",
+ "engine-chromeicon",
+ "engine-same-name-gd",
+ "engine",
+ "engine-reordered",
+ ],
+ "Should have the correct list of engines installed in the expected order."
+ );
+
+ const newDefault = await defaultEngineChanged;
+ Assert.equal(
+ newDefault.QueryInterface(Ci.nsISearchEngine).name,
+ "engine-pref",
+ "Should have correctly notified the new default engine"
+ );
+
+ const newDefaultPrivate = await defaultPrivateEngineChanged;
+ Assert.equal(
+ newDefaultPrivate.QueryInterface(Ci.nsISearchEngine).name,
+ "engine-pref",
+ "Should have correctly notified the new default private engine"
+ );
+
+ const engineWithParams = await Services.search.getEngineByName(
+ "engine-chromeicon"
+ );
+ Assert.equal(
+ engineWithParams.getSubmission("test").uri.spec,
+ "https://www.google.com/search?c=my-test&q1=test",
+ "Should have updated the parameters"
+ );
+
+ const engineWithSameName = await Services.search.getEngineByName(
+ "engine-same-name"
+ );
+ Assert.equal(
+ engineWithSameName.getSubmission("test").uri.spec,
+ "https://www.example.com/search?q=test",
+ "Should have correctly switched to the engine of the same name"
+ );
+
+ Assert.equal(
+ Services.search.wrappedJSObject._settings.getMetaDataAttribute(
+ "useSavedOrder"
+ ),
+ false,
+ "Should not have set the useSavedOrder preference"
+ );
+});
+
+add_task(async function test_user_settings_persist() {
+ let reload = SearchTestUtils.promiseSearchNotification("engines-reloaded");
+ Region._setHomeRegion("");
+ await reload;
+
+ Assert.ok(
+ (await visibleEngines()).includes("engine-rel-searchform-purpose"),
+ "Rel Searchform engine should be included by default"
+ );
+
+ let settingsFileWritten = promiseAfterSettings();
+ let engine = await Services.search.getEngineByName(
+ "engine-rel-searchform-purpose"
+ );
+ await Services.search.removeEngine(engine);
+ await settingsFileWritten;
+
+ Assert.ok(
+ !(await visibleEngines()).includes("engine-rel-searchform-purpose"),
+ "Rel Searchform engine has been removed"
+ );
+
+ reload = SearchTestUtils.promiseSearchNotification("engines-reloaded");
+ Region._setHomeRegion("FR");
+ await reload;
+
+ reload = SearchTestUtils.promiseSearchNotification("engines-reloaded");
+ Region._setHomeRegion("");
+ await reload;
+
+ Assert.ok(
+ !(await visibleEngines()).includes("engine-rel-searchform-purpose"),
+ "Rel Searchform removal should be remembered"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_reload_engines_experiment.js b/toolkit/components/search/tests/xpcshell/test_reload_engines_experiment.js
new file mode 100644
index 0000000000..ded610ca56
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_reload_engines_experiment.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const CONFIG = [
+ {
+ // Just a basic engine that won't be changed.
+ webExtension: {
+ id: "engine@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ default: "yes",
+ },
+ ],
+ },
+ {
+ // This engine will have the locale swapped when the experiment is set.
+ webExtension: {
+ id: "engine-same-name@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ webExtension: {
+ locales: ["en"],
+ },
+ },
+ {
+ included: { everywhere: true },
+ webExtension: {
+ locales: ["gd"],
+ },
+ experiment: "xpcshell",
+ },
+ ],
+ },
+];
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("data", null, CONFIG);
+ await AddonTestUtils.promiseStartupManager();
+});
+
+// This is to verify that the loaded configuration matches what we expect for
+// the test.
+add_task(async function test_initial_config_correct() {
+ await Services.search.init();
+
+ const installedEngines = await Services.search.getAppProvidedEngines();
+ Assert.deepEqual(
+ installedEngines.map(e => e.identifier),
+ ["engine", "engine-same-name-en"],
+ "Should have the correct list of engines installed."
+ );
+
+ Assert.equal(
+ (await Services.search.getDefault()).identifier,
+ "engine",
+ "Should have loaded the expected default engine"
+ );
+});
+
+add_task(async function test_config_updated_engine_changes() {
+ // Update the config.
+ const reloadObserved =
+ SearchTestUtils.promiseSearchNotification("engines-reloaded");
+ const enginesAdded = [];
+ const enginesModified = [];
+ const enginesRemoved = [];
+
+ function enginesObs(subject, topic, data) {
+ if (data == SearchUtils.MODIFIED_TYPE.ADDED) {
+ enginesAdded.push(subject.QueryInterface(Ci.nsISearchEngine).identifier);
+ } else if (data == SearchUtils.MODIFIED_TYPE.CHANGED) {
+ enginesModified.push(
+ subject.QueryInterface(Ci.nsISearchEngine).identifier
+ );
+ } else if (data == SearchUtils.MODIFIED_TYPE.REMOVED) {
+ enginesRemoved.push(subject.QueryInterface(Ci.nsISearchEngine).name);
+ }
+ }
+ Services.obs.addObserver(enginesObs, SearchUtils.TOPIC_ENGINE_MODIFIED);
+
+ Services.prefs.setCharPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "experiment",
+ "xpcshell"
+ );
+
+ await reloadObserved;
+ Services.obs.removeObserver(enginesObs, SearchUtils.TOPIC_ENGINE_MODIFIED);
+
+ Assert.deepEqual(enginesAdded, [], "Should have added the correct engines");
+
+ Assert.deepEqual(
+ enginesModified.sort(),
+ ["engine", "engine-same-name-gd"],
+ "Should have modified the expected engines"
+ );
+
+ Assert.deepEqual(
+ enginesRemoved,
+ [],
+ "Should have removed the expected engine"
+ );
+
+ const installedEngines = await Services.search.getAppProvidedEngines();
+
+ Assert.deepEqual(
+ installedEngines.map(e => e.identifier),
+ ["engine", "engine-same-name-gd"],
+ "Should have the correct list of engines installed in the expected order."
+ );
+
+ const engineWithSameName = await Services.search.getEngineByName(
+ "engine-same-name"
+ );
+ Assert.equal(
+ engineWithSameName.getSubmission("test").uri.spec,
+ "https://www.example.com/search?q=test",
+ "Should have correctly switched to the engine of the same name"
+ );
+
+ Assert.equal(
+ Services.search.wrappedJSObject._settings.getMetaDataAttribute(
+ "useSavedOrder"
+ ),
+ false,
+ "Should not have set the useSavedOrder preference"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_reload_engines_locales.js b/toolkit/components/search/tests/xpcshell/test_reload_engines_locales.js
new file mode 100644
index 0000000000..9e98e5e73a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_reload_engines_locales.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests reloading engines when changing the in-use locale of a WebExtension,
+ * where the name of the engine changes as well.
+ */
+
+"use strict";
+
+const CONFIG = [
+ {
+ webExtension: {
+ id: "engine@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ default: "yes",
+ },
+ ],
+ },
+ {
+ webExtension: {
+ id: "engine-diff-name@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ excluded: { locales: { matches: ["gd"] } },
+ },
+ {
+ included: { locales: { matches: ["gd"] } },
+ webExtension: {
+ locales: ["gd"],
+ },
+ },
+ ],
+ },
+];
+
+add_setup(async () => {
+ Services.locale.availableLocales = [
+ ...Services.locale.availableLocales,
+ "en",
+ "gd",
+ ];
+ Services.locale.requestedLocales = ["gd"];
+
+ await SearchTestUtils.useTestEngines("data", null, CONFIG);
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+});
+
+add_task(async function test_config_updated_engine_changes() {
+ let engines = await Services.search.getEngines();
+ Assert.deepEqual(
+ engines.map(e => e.name),
+ ["Test search engine", "engine-diff-name-gd"],
+ "Should have the correct engines installed"
+ );
+
+ let engine = await Services.search.getEngineByName("engine-diff-name-gd");
+ Assert.equal(
+ engine.name,
+ "engine-diff-name-gd",
+ "Should have the correct engine name"
+ );
+ Assert.equal(
+ engine.getSubmission("test").uri.spec,
+ "https://gd.wikipedia.com/search",
+ "Should have the gd search url"
+ );
+
+ await promiseSetLocale("en");
+
+ engines = await Services.search.getEngines();
+ Assert.deepEqual(
+ engines.map(e => e.name),
+ ["Test search engine", "engine-diff-name-en"],
+ "Should have the correct engines installed after locale change"
+ );
+
+ engine = await Services.search.getEngineByName("engine-diff-name-en");
+ Assert.equal(
+ engine.name,
+ "engine-diff-name-en",
+ "Should have the correct engine name"
+ );
+ Assert.equal(
+ engine.getSubmission("test").uri.spec,
+ "https://en.wikipedia.com/search",
+ "Should have the en search url"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_remove_engine_notification_box.js b/toolkit/components/search/tests/xpcshell/test_remove_engine_notification_box.js
new file mode 100644
index 0000000000..867412a771
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_remove_engine_notification_box.js
@@ -0,0 +1,392 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.importGlobalProperties(["structuredClone"]);
+
+const CONFIG = [
+ {
+ // Engine initially default, but the defaults will be changed to engine-pref.
+ webExtension: {
+ id: "engine@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ default: "yes",
+ },
+ {
+ included: { regions: ["FR"] },
+ default: "no",
+ },
+ ],
+ },
+ {
+ // This will become defaults when region is changed to FR.
+ webExtension: {
+ id: "engine-pref@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ {
+ included: { regions: ["FR"] },
+ default: "yes",
+ },
+ ],
+ },
+];
+
+const CONFIG_UPDATED = [
+ {
+ webExtension: {
+ id: "engine-pref@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ {
+ included: { regions: ["FR"] },
+ default: "yes",
+ },
+ ],
+ },
+];
+
+let stub;
+let settingsFilePath;
+let userSettings;
+
+add_task(async function setup() {
+ SearchSettings.SETTINGS_INVALIDATION_DELAY = 100;
+ SearchTestUtils.useMockIdleService();
+ await SearchTestUtils.useTestEngines("data", null, CONFIG);
+ await AddonTestUtils.promiseStartupManager();
+
+ stub = sinon.stub(
+ await Services.search.wrappedJSObject,
+ "_showRemovalOfSearchEngineNotificationBox"
+ );
+
+ settingsFilePath = PathUtils.join(PathUtils.profileDir, SETTINGS_FILENAME);
+
+ Region._setHomeRegion("", false);
+
+ let promiseSaved = promiseAfterSettings();
+ await Services.search.init();
+ await promiseSaved;
+
+ userSettings = await Services.search.wrappedJSObject._settings.get();
+});
+
+// Verify the loaded configuration matches what we expect for the test.
+add_task(async function test_initial_config_correct() {
+ const installedEngines = await Services.search.getAppProvidedEngines();
+ Assert.deepEqual(
+ installedEngines.map(e => e.identifier),
+ ["engine", "engine-pref"],
+ "Should have the correct list of engines installed."
+ );
+
+ Assert.equal(
+ (await Services.search.getDefault()).identifier,
+ "engine",
+ "Should have loaded the expected default engine"
+ );
+});
+
+add_task(async function test_metadata_undefined() {
+ let defaultEngineChanged = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.DEFAULT,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+
+ info("Update region to FR.");
+ Region._setHomeRegion("FR", false);
+
+ let settings = structuredClone(userSettings);
+ settings.metaData = undefined;
+ await reloadEngines(settings);
+ Assert.ok(
+ stub.notCalled,
+ "_reloadEngines should not have shown the notification box."
+ );
+
+ settings = structuredClone(userSettings);
+ settings.metaData = undefined;
+ await loadEngines(settings);
+ Assert.ok(
+ stub.notCalled,
+ "_loadEngines should not have shown the notification box."
+ );
+
+ const newDefault = await defaultEngineChanged;
+ Assert.equal(
+ newDefault.QueryInterface(Ci.nsISearchEngine).name,
+ "engine-pref",
+ "Should have correctly notified the new default engine."
+ );
+});
+
+add_task(async function test_metadata_changed() {
+ let metaDataProperties = [
+ "locale",
+ "region",
+ "channel",
+ "experiment",
+ "distroID",
+ ];
+
+ for (let name of metaDataProperties) {
+ let settings = structuredClone(userSettings);
+ settings.metaData[name] = "test";
+ await assert_metadata_changed(settings);
+ }
+});
+
+add_task(async function test_default_engine_unchanged() {
+ let currentEngineName =
+ Services.search.wrappedJSObject._getEngineDefault(false).name;
+
+ Assert.equal(
+ currentEngineName,
+ "Test search engine",
+ "Default engine should be unchanged."
+ );
+
+ await reloadEngines(structuredClone(userSettings));
+ Assert.ok(
+ stub.notCalled,
+ "_reloadEngines should not have shown the notification box."
+ );
+
+ await loadEngines(structuredClone(userSettings));
+ Assert.ok(
+ stub.notCalled,
+ "_loadEngines should not have shown the notification box."
+ );
+});
+
+add_task(async function test_new_current_engine_is_undefined() {
+ consoleAllowList.push("No default engine");
+ let settings = structuredClone(userSettings);
+ let getEngineDefaultStub = sinon.stub(
+ await Services.search.wrappedJSObject,
+ "_getEngineDefault"
+ );
+ getEngineDefaultStub.returns(undefined);
+
+ await loadEngines(settings);
+ Assert.ok(
+ stub.notCalled,
+ "_loadEngines should not have shown the notification box."
+ );
+
+ getEngineDefaultStub.restore();
+});
+
+add_task(async function test_current_engine_is_null() {
+ Services.search.wrappedJSObject._currentEngine = null;
+
+ await reloadEngines(structuredClone(userSettings));
+ Assert.ok(
+ stub.notCalled,
+ "_reloadEngines should not have shown the notification box."
+ );
+
+ let settings = structuredClone(userSettings);
+ settings.metaData.current = null;
+ await loadEngines(settings);
+ Assert.ok(
+ stub.notCalled,
+ "_loadEngines should not have shown the notification box."
+ );
+});
+
+add_task(async function test_default_changed_and_metadata_unchanged_exists() {
+ info("Update region to FR to change engine.");
+ Region._setHomeRegion("FR", false);
+
+ info("Set user settings metadata to the same properties as cached metadata.");
+ await Services.search.wrappedJSObject._fetchEngineSelectorEngines();
+ userSettings.metaData = {
+ ...Services.search.wrappedJSObject._settings.getSettingsMetaData(),
+ appDefaultEngine: "Test search engine",
+ };
+
+ await reloadEngines(structuredClone(userSettings));
+ Assert.ok(
+ stub.notCalled,
+ "_reloadEngines should not show the notification box as the engine still exists."
+ );
+
+ // Reset.
+ Region._setHomeRegion("US", false);
+ await reloadEngines(structuredClone(userSettings));
+});
+
+add_task(async function test_default_engine_changed_and_metadata_unchanged() {
+ info("Update region to FR to change engine.");
+ Region._setHomeRegion("FR", false);
+
+ const defaultEngineChanged = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.DEFAULT,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+
+ info("Set user settings metadata to the same properties as cached metadata.");
+ await Services.search.wrappedJSObject._fetchEngineSelectorEngines();
+ userSettings.metaData = {
+ ...Services.search.wrappedJSObject._settings.getSettingsMetaData(),
+ appDefaultEngineId: "engine@search.mozilla.orgdefault",
+ };
+
+ // Update config by removing the app default engine
+ await setConfigToLoad(CONFIG_UPDATED);
+
+ await reloadEngines(structuredClone(userSettings));
+ Assert.ok(
+ stub.calledOnce,
+ "_reloadEngines should show the notification box."
+ );
+
+ Assert.deepEqual(
+ stub.firstCall.args,
+ ["Test search engine", "engine-pref"],
+ "_showRemovalOfSearchEngineNotificationBox should display " +
+ "'Test search engine' as the engine removed and 'engine-pref' as the new " +
+ "default engine."
+ );
+
+ const newDefault = await defaultEngineChanged;
+ Assert.equal(
+ newDefault.QueryInterface(Ci.nsISearchEngine).name,
+ "engine-pref",
+ "Should have correctly notified the new default engine"
+ );
+
+ info("Reset userSettings.metaData.current engine.");
+ let settings = structuredClone(userSettings);
+ settings.metaData.current = Services.search.wrappedJSObject._currentEngine;
+
+ await loadEngines(settings);
+ Assert.ok(stub.calledTwice, "_loadEngines should show the notification box.");
+
+ Assert.deepEqual(
+ stub.secondCall.args,
+ ["Test search engine", "engine-pref"],
+ "_showRemovalOfSearchEngineNotificationBox should display " +
+ "'Test search engine' as the engine removed and 'engine-pref' as the new " +
+ "default engine."
+ );
+});
+
+add_task(async function test_app_default_engine_changed_on_start_up() {
+ let settings = structuredClone(userSettings);
+
+ // Set the current engine to "" so we can use the app default engine as
+ // default
+ settings.metaData.current = "";
+
+ // Update config by removing the app default engine
+ await setConfigToLoad(CONFIG_UPDATED);
+
+ await loadEngines(settings);
+ Assert.ok(
+ stub.calledThrice,
+ "_loadEngines should show the notification box."
+ );
+});
+
+add_task(async function test_app_default_engine_change_start_up_still_exists() {
+ stub.resetHistory();
+ let settings = structuredClone(userSettings);
+
+ // Set the current engine to "" so we can use the app default engine as
+ // default
+ settings.metaData.current = "";
+ settings.metaData.appDefaultEngine = "Test search engine";
+
+ await setConfigToLoad(CONFIG);
+
+ await loadEngines(settings);
+ Assert.ok(
+ stub.notCalled,
+ "_loadEngines should not show the notification box."
+ );
+});
+
+async function setConfigToLoad(config) {
+ let searchSettingsObj = await RemoteSettings(SearchUtils.SETTINGS_KEY);
+ // Restore the get method in order to stub it again in useTestEngines
+ searchSettingsObj.get.restore();
+ Services.search.wrappedJSObject.resetEngineSelector();
+ await SearchTestUtils.useTestEngines("data", null, config);
+}
+
+function writeSettings(settings) {
+ return IOUtils.writeJSON(settingsFilePath, settings, { compress: true });
+}
+
+async function reloadEngines(settings) {
+ let promiseSaved = promiseAfterSettings();
+
+ await Services.search.wrappedJSObject._reloadEngines(settings);
+
+ await promiseSaved;
+}
+
+async function loadEngines(settings) {
+ await writeSettings(settings);
+
+ let promiseSaved = promiseAfterSettings();
+
+ Services.search.wrappedJSObject.reset();
+ await Services.search.init();
+
+ await promiseSaved;
+}
+
+async function assert_metadata_changed(settings) {
+ info("Update region.");
+ Region._setHomeRegion("FR", false);
+ await reloadEngines(settings);
+ Region._setHomeRegion("", false);
+
+ let defaultEngineChanged = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.DEFAULT,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+
+ await reloadEngines(settings);
+ Assert.ok(
+ stub.notCalled,
+ "_reloadEngines should not have shown the notification box."
+ );
+
+ let newDefault = await defaultEngineChanged;
+ Assert.equal(
+ newDefault.QueryInterface(Ci.nsISearchEngine).name,
+ "Test search engine",
+ "Should have correctly notified the new default engine."
+ );
+
+ Region._setHomeRegion("FR", false);
+ await reloadEngines(settings);
+ Region._setHomeRegion("", false);
+
+ await loadEngines(settings);
+ Assert.ok(
+ stub.notCalled,
+ "_loadEngines should not have shown the notification box."
+ );
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "Test search engine",
+ "Should have correctly notified the new default engine."
+ );
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js b/toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js
new file mode 100644
index 0000000000..76a67b39c2
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test is to ensure that we remove xml files from searchplugins/ in the
+// profile directory when a user removes the actual engine from their profile.
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("data1");
+ await AddonTestUtils.promiseStartupManager();
+});
+
+add_task(async function run_test() {
+ // Copy an engine to [profile]/searchplugin/
+ let dir = do_get_profile().clone();
+ dir.append("searchplugins");
+ if (!dir.exists()) {
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ }
+ do_get_file("data/engine.xml").copyTo(dir, "test-search-engine.xml");
+
+ let file = dir.clone();
+ file.append("test-search-engine.xml");
+ Assert.ok(file.exists());
+
+ let data = await readJSONFile(do_get_file("data/search-legacy.json"));
+
+ // Put the filePath inside the settings file, to simulate what a pre-58 version
+ // of Firefox would have done.
+ for (let engine of data.engines) {
+ if (engine._name == "Test search engine") {
+ engine.filePath = file.path;
+ }
+ }
+
+ await promiseSaveSettingsData(data);
+
+ await Services.search.init();
+
+ // test the engine is loaded ok.
+ let engine = Services.search.getEngineByName("Test search engine");
+ Assert.notEqual(engine, null, "Should have found the engine");
+
+ // remove the engine and verify the file has been removed too.
+ await Services.search.removeEngine(engine);
+ Assert.ok(!file.exists(), "Should have removed the file.");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_save_sorted_engines.js b/toolkit/components/search/tests/xpcshell/test_save_sorted_engines.js
new file mode 100644
index 0000000000..b609960d1f
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_save_sorted_engines.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Ensure that metadata are stored correctly on disk after:
+ * - moving an engine
+ * - removing an engine
+ * - adding a new engine
+ *
+ * Notes:
+ * - we install the search engines of test "test_downloadAndAddEngines.js"
+ * to ensure that this test is independent from locale, commercial agreements
+ * and configuration of Firefox.
+ */
+
+add_task(async function setup() {
+ useHttpServer();
+ await AddonTestUtils.promiseStartupManager();
+});
+
+add_task(async function test_save_sorted_engines() {
+ let engine1 = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engine.xml`,
+ });
+ let engine2 = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engine2.xml`,
+ });
+ await promiseAfterSettings();
+
+ let search = Services.search;
+
+ // Test moving the engines
+ await search.moveEngine(engine1, 0);
+ await search.moveEngine(engine2, 1);
+
+ // Changes should be commited immediately
+ await promiseAfterSettings();
+ info("Commit complete after moveEngine");
+
+ // Check that the entries are placed as specified correctly
+ let metadata = await promiseEngineMetadata();
+ Assert.equal(metadata["Test search engine"].order, 1);
+ Assert.equal(metadata["A second test engine"].order, 2);
+
+ // Test removing an engine
+ search.removeEngine(engine1);
+ await promiseAfterSettings();
+ info("Commit complete after removeEngine");
+
+ // Check that the order of the remaining engine was updated correctly
+ metadata = await promiseEngineMetadata();
+ Assert.equal(metadata["A second test engine"].order, 1);
+
+ // Test adding a new engine
+ await SearchTestUtils.installSearchExtension({
+ name: "foo",
+ keyword: "foo",
+ });
+
+ let engine = Services.search.getEngineByName("foo");
+ await promiseAfterSettings();
+ info("Commit complete after addEngineWithDetails");
+
+ metadata = await promiseEngineMetadata();
+ Assert.ok(engine.aliases.includes("foo"));
+ Assert.ok(metadata.foo.order > 0);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_searchSuggest.js b/toolkit/components/search/tests/xpcshell/test_searchSuggest.js
new file mode 100644
index 0000000000..161e7d6c63
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_searchSuggest.js
@@ -0,0 +1,891 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+/**
+ * Testing search suggestions from SearchSuggestionController.jsm.
+ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { FormHistory } = ChromeUtils.importESModule(
+ "resource://gre/modules/FormHistory.sys.mjs"
+);
+const { SearchSuggestionController } = ChromeUtils.importESModule(
+ "resource://gre/modules/SearchSuggestionController.sys.mjs"
+);
+const { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const ENGINE_NAME = "other";
+const SEARCH_TELEMETRY_LATENCY = "SEARCH_SUGGESTIONS_LATENCY_MS";
+
+// We must make sure the FormHistoryStartup component is
+// initialized in order for it to respond to FormHistory
+// requests from nsFormAutoComplete.js.
+var formHistoryStartup = Cc[
+ "@mozilla.org/satchel/form-history-startup;1"
+].getService(Ci.nsIObserver);
+formHistoryStartup.observe(null, "profile-after-change", null);
+
+var getEngine, postEngine, unresolvableEngine, alternateJSONEngine;
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+ // These tests intentionally test broken connections.
+ consoleAllowList = consoleAllowList.concat([
+ "Non-200 status or empty HTTP response: 404",
+ "Non-200 status or empty HTTP response: 500",
+ "Unexpected response, searchString does not match remote response",
+ "HTTP request timeout",
+ "HTTP error",
+ ]);
+
+ let server = useHttpServer();
+ server.registerContentType("sjs", "sjs");
+
+ await AddonTestUtils.promiseStartupManager();
+
+ registerCleanupFunction(async () => {
+ // Remove added form history entries
+ await updateSearchHistory("remove", null);
+ Services.prefs.clearUserPref("browser.search.suggest.enabled");
+ });
+});
+
+add_task(async function add_test_engines() {
+ let getEngineData = {
+ baseURL: gDataUrl,
+ name: "GET suggestion engine",
+ method: "GET",
+ };
+
+ let postEngineData = {
+ baseURL: gDataUrl,
+ name: "POST suggestion engine",
+ method: "POST",
+ };
+
+ let unresolvableEngineData = {
+ baseURL: "http://example.invalid/",
+ name: "Offline suggestion engine",
+ method: "GET",
+ };
+
+ let alternateJSONSuggestEngineData = {
+ baseURL: gDataUrl,
+ name: "Alternative JSON suggestion type",
+ method: "GET",
+ alternativeJSONType: true,
+ };
+
+ getEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engineMaker.sjs?${JSON.stringify(getEngineData)}`,
+ });
+ postEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engineMaker.sjs?${JSON.stringify(postEngineData)}`,
+ });
+ unresolvableEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engineMaker.sjs?${JSON.stringify(unresolvableEngineData)}`,
+ });
+ alternateJSONEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engineMaker.sjs?${JSON.stringify(
+ alternateJSONSuggestEngineData
+ )}`,
+ });
+});
+
+// Begin tests
+
+add_task(async function simple_no_result_promise() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("no remote", false, getEngine);
+ Assert.equal(result.term, "no remote");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 0);
+
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function simple_remote_no_local_result() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("mo", false, getEngine);
+ Assert.equal(result.term, "mo");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 3);
+ Assert.equal(result.remote[0].value, "Mozilla");
+ Assert.equal(result.remote[1].value, "modern");
+ Assert.equal(result.remote[2].value, "mom");
+
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function simple_remote_no_local_result_telemetry() {
+ Services.telemetry.clearScalars();
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ await controller.fetch("mo", false, getEngine);
+
+ let scalars = {};
+ const key = "browser.search.data_transferred";
+
+ await TestUtils.waitForCondition(() => {
+ scalars =
+ Services.telemetry.getSnapshotForKeyedScalars("main", false).parent || {};
+ return key in scalars;
+ }, "should have the expected keyed scalars");
+
+ const scalar = scalars[key];
+ Assert.ok(`sggt-${ENGINE_NAME}` in scalar, "correct telemetry category");
+ Assert.notEqual(scalar[`sggt-${ENGINE_NAME}`], 0, "bandwidth logged");
+
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function simple_remote_no_local_result_alternative_type() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("mo", false, alternateJSONEngine);
+ Assert.equal(result.term, "mo");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 3);
+ Assert.equal(result.remote[0].value, "Mozilla");
+ Assert.equal(result.remote[1].value, "modern");
+ Assert.equal(result.remote[2].value, "mom");
+});
+
+add_task(async function remote_term_case_mismatch() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("Query Case Mismatch", false, getEngine);
+ Assert.equal(result.term, "Query Case Mismatch");
+ Assert.equal(result.remote.length, 1);
+ Assert.equal(result.remote[0].value, "Query Case Mismatch");
+});
+
+add_task(async function simple_local_no_remote_result() {
+ await updateSearchHistory("bump", "no remote entries");
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("no remote", false, getEngine);
+ Assert.equal(result.term, "no remote");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "no remote entries");
+ Assert.equal(result.remote.length, 0);
+
+ await updateSearchHistory("remove", "no remote entries");
+});
+
+add_task(async function simple_non_ascii() {
+ await updateSearchHistory("bump", "I ❤️ XUL");
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("I ❤️", false, getEngine);
+ Assert.equal(result.term, "I ❤️");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "I ❤️ XUL");
+ Assert.equal(result.remote.length, 1);
+ Assert.equal(result.remote[0].value, "I ❤️ Mozilla");
+});
+
+add_task(async function both_local_remote_result_dedupe() {
+ await updateSearchHistory("bump", "Mozilla");
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("mo", false, getEngine);
+ Assert.equal(result.term, "mo");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "Mozilla");
+ Assert.equal(result.remote.length, 2);
+ Assert.equal(result.remote[0].value, "modern");
+ Assert.equal(result.remote[1].value, "mom");
+});
+
+add_task(async function POST_both_local_remote_result_dedupe() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("mo", false, postEngine);
+ Assert.equal(result.term, "mo");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "Mozilla");
+ Assert.equal(result.remote.length, 2);
+ Assert.equal(result.remote[0].value, "modern");
+ Assert.equal(result.remote[1].value, "mom");
+});
+
+add_task(async function both_local_remote_result_dedupe2() {
+ await updateSearchHistory("bump", "mom");
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("mo", false, getEngine);
+ Assert.equal(result.term, "mo");
+ Assert.equal(result.local.length, 2);
+ Assert.equal(result.local[0].value, "mom");
+ Assert.equal(result.local[1].value, "Mozilla");
+ Assert.equal(result.remote.length, 1);
+ Assert.equal(result.remote[0].value, "modern");
+});
+
+add_task(async function both_local_remote_result_dedupe3() {
+ // All of the server entries also exist locally
+ await updateSearchHistory("bump", "modern");
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("mo", false, getEngine);
+ Assert.equal(result.term, "mo");
+ Assert.equal(result.local.length, 3);
+ Assert.equal(result.local[0].value, "modern");
+ Assert.equal(result.local[1].value, "mom");
+ Assert.equal(result.local[2].value, "Mozilla");
+ Assert.equal(result.remote.length, 0);
+});
+
+add_task(async function valid_tail_results() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("tail query", false, getEngine);
+ Assert.equal(result.term, "tail query");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 3);
+ Assert.equal(result.remote[0].value, "tail query normal");
+ Assert.ok(!result.remote[0].matchPrefix);
+ Assert.ok(!result.remote[0].tail);
+ Assert.equal(result.remote[1].value, "tail query tail 1");
+ Assert.equal(result.remote[1].matchPrefix, "… ");
+ Assert.equal(result.remote[1].tail, "tail 1");
+ Assert.equal(result.remote[2].value, "tail query tail 2");
+ Assert.equal(result.remote[2].matchPrefix, "… ");
+ Assert.equal(result.remote[2].tail, "tail 2");
+});
+
+add_task(async function alt_tail_results() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("tailalt query", false, getEngine);
+ Assert.equal(result.term, "tailalt query");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 3);
+ Assert.equal(result.remote[0].value, "tailalt query normal");
+ Assert.ok(!result.remote[0].matchPrefix);
+ Assert.ok(!result.remote[0].tail);
+ Assert.equal(result.remote[1].value, "tailalt query tail 1");
+ Assert.equal(result.remote[1].matchPrefix, "… ");
+ Assert.equal(result.remote[1].tail, "tail 1");
+ Assert.equal(result.remote[2].value, "tailalt query tail 2");
+ Assert.equal(result.remote[2].matchPrefix, "… ");
+ Assert.equal(result.remote[2].tail, "tail 2");
+});
+
+add_task(async function invalid_tail_results() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("tailjunk query", false, getEngine);
+ Assert.equal(result.term, "tailjunk query");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 3);
+ Assert.equal(result.remote[0].value, "tailjunk query normal");
+ Assert.ok(!result.remote[0].matchPrefix);
+ Assert.ok(!result.remote[0].tail);
+ Assert.equal(result.remote[1].value, "tailjunk query tail 1");
+ Assert.ok(!result.remote[1].matchPrefix);
+ Assert.ok(!result.remote[1].tail);
+ Assert.equal(result.remote[2].value, "tailjunk query tail 2");
+ Assert.ok(!result.remote[2].matchPrefix);
+ Assert.ok(!result.remote[2].tail);
+});
+
+add_task(async function too_few_tail_results() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("tailjunk few query", false, getEngine);
+ Assert.equal(result.term, "tailjunk few query");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 3);
+ Assert.equal(result.remote[0].value, "tailjunk few query normal");
+ Assert.ok(!result.remote[0].matchPrefix);
+ Assert.ok(!result.remote[0].tail);
+ Assert.equal(result.remote[1].value, "tailjunk few query tail 1");
+ Assert.ok(!result.remote[1].matchPrefix);
+ Assert.ok(!result.remote[1].tail);
+ Assert.equal(result.remote[2].value, "tailjunk few query tail 2");
+ Assert.ok(!result.remote[2].matchPrefix);
+ Assert.ok(!result.remote[2].tail);
+});
+
+add_task(async function empty_rich_results() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("richempty query", false, getEngine);
+ Assert.equal(result.term, "richempty query");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 3);
+ Assert.equal(result.remote[0].value, "richempty query normal");
+ Assert.ok(!result.remote[0].matchPrefix);
+ Assert.ok(!result.remote[0].tail);
+ Assert.equal(result.remote[1].value, "richempty query tail 1");
+ Assert.ok(!result.remote[1].matchPrefix);
+ Assert.ok(!result.remote[1].tail);
+ Assert.equal(result.remote[2].value, "richempty query tail 2");
+ Assert.ok(!result.remote[2].matchPrefix);
+ Assert.ok(!result.remote[2].tail);
+});
+
+add_task(async function tail_offset_index() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("tail tail 1 t", false, getEngine);
+ Assert.equal(result.term, "tail tail 1 t");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 3);
+ Assert.equal(result.remote[1].value, "tail tail 1 t tail 1");
+ Assert.equal(result.remote[1].matchPrefix, "… ");
+ Assert.equal(result.remote[1].tail, "tail 1");
+ Assert.equal(result.remote[1].tailOffsetIndex, 14);
+});
+
+add_task(async function fetch_twice_in_a_row() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ // Two entries since the first will match the first fetch but not the second.
+ await updateSearchHistory("bump", "delay local");
+ await updateSearchHistory("bump", "delayed local");
+
+ let controller = new SearchSuggestionController();
+ let resultPromise1 = controller.fetch("delay", false, getEngine);
+
+ // A second fetch while the server is still waiting to return results leads to an abort.
+ let resultPromise2 = controller.fetch("delayed ", false, getEngine);
+ await resultPromise1.then(results => Assert.equal(null, results));
+
+ let result = await resultPromise2;
+ Assert.equal(result.term, "delayed ");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "delayed local");
+ Assert.equal(result.remote.length, 1);
+ Assert.equal(result.remote[0].value, "delayed ");
+
+ // Only the second fetch's latency should be recorded since the first fetch
+ // was aborted and latencies for aborted fetches are not recorded.
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function both_identical_with_more_than_max_results() {
+ // Add letters A through Z to form history which will match the server
+ for (
+ let charCode = "A".charCodeAt();
+ charCode <= "Z".charCodeAt();
+ charCode++
+ ) {
+ await updateSearchHistory(
+ "bump",
+ "letter " + String.fromCharCode(charCode)
+ );
+ }
+
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 7;
+ controller.maxRemoteResults = 10;
+ let result = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 7);
+ for (let i = 0; i < controller.maxLocalResults; i++) {
+ Assert.equal(
+ result.local[i].value,
+ "letter " + String.fromCharCode("A".charCodeAt() + i)
+ );
+ }
+ Assert.equal(result.local.length + result.remote.length, 10);
+ for (let i = 0; i < result.remote.length; i++) {
+ Assert.equal(
+ result.remote[i].value,
+ "letter " +
+ String.fromCharCode("A".charCodeAt() + controller.maxLocalResults + i)
+ );
+ }
+});
+
+add_task(async function noremote_maxLocal() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 2; // (should be ignored because no remote results)
+ controller.maxRemoteResults = 0;
+ let result = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 26);
+ for (let i = 0; i < result.local.length; i++) {
+ Assert.equal(
+ result.local[i].value,
+ "letter " + String.fromCharCode("A".charCodeAt() + i)
+ );
+ }
+ Assert.equal(result.remote.length, 0);
+
+ assertLatencyHistogram(histogram, false);
+});
+
+add_task(async function someremote_maxLocal() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 2;
+ controller.maxRemoteResults = 4;
+ let result = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 2);
+ for (let i = 0; i < result.local.length; i++) {
+ Assert.equal(
+ result.local[i].value,
+ "letter " + String.fromCharCode("A".charCodeAt() + i)
+ );
+ }
+ Assert.equal(result.remote.length, 2);
+ // "A" and "B" will have been de-duped, start at C for remote results
+ for (let i = 0; i < result.remote.length; i++) {
+ Assert.equal(
+ result.remote[i].value,
+ "letter " + String.fromCharCode("C".charCodeAt() + i)
+ );
+ }
+
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function one_of_each() {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 2;
+ let result = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "letter A");
+ Assert.equal(result.remote.length, 1);
+ Assert.equal(result.remote[0].value, "letter B");
+});
+
+add_task(async function local_result_returned_remote_result_disabled() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 1;
+ let result = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 26);
+ for (let i = 0; i < 26; i++) {
+ Assert.equal(
+ result.local[i].value,
+ "letter " + String.fromCharCode("A".charCodeAt() + i)
+ );
+ }
+ Assert.equal(result.remote.length, 0);
+ assertLatencyHistogram(histogram, false);
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+});
+
+add_task(
+ async function local_result_returned_remote_result_disabled_after_creation_of_controller() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 1;
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+ let result = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 26);
+ for (let i = 0; i < 26; i++) {
+ Assert.equal(
+ result.local[i].value,
+ "letter " + String.fromCharCode("A".charCodeAt() + i)
+ );
+ }
+ Assert.equal(result.remote.length, 0);
+ assertLatencyHistogram(histogram, false);
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+ }
+);
+
+add_task(
+ async function one_of_each_disabled_before_creation_enabled_after_creation_of_controller() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 2;
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+ let result = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "letter A");
+ Assert.equal(result.remote.length, 1);
+ Assert.equal(result.remote[0].value, "letter B");
+
+ assertLatencyHistogram(histogram, true);
+
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+ }
+);
+
+add_task(async function one_local_zero_remote() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 0;
+ let result = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 26);
+ for (let i = 0; i < 26; i++) {
+ Assert.equal(
+ result.local[i].value,
+ "letter " + String.fromCharCode("A".charCodeAt() + i)
+ );
+ }
+ Assert.equal(result.remote.length, 0);
+ assertLatencyHistogram(histogram, false);
+});
+
+add_task(async function zero_local_one_remote() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 0;
+ controller.maxRemoteResults = 1;
+ let result = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 1);
+ Assert.equal(result.remote[0].value, "letter A");
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function stop_search() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+ let controller = new SearchSuggestionController(result => {
+ do_throw("The callback shouldn't be called after stop()");
+ });
+ let resultPromise = controller.fetch("mo", false, getEngine);
+ controller.stop();
+ await resultPromise.then(result => {
+ Assert.equal(null, result);
+ });
+ assertLatencyHistogram(histogram, false);
+});
+
+add_task(async function empty_searchTerm() {
+ // Empty searches don't go to the server but still get form history.
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("", false, getEngine);
+ Assert.equal(result.term, "");
+ Assert.ok(!!result.local.length);
+ Assert.equal(result.remote.length, 0);
+ assertLatencyHistogram(histogram, false);
+});
+
+add_task(async function slow_timeout() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ // Make the server return suggestions on a delay longer than the timeout of
+ // the suggestion controller.
+ let delayMs = 3 * SearchSuggestionController.REMOTE_TIMEOUT_DEFAULT;
+ let searchString = `delay${delayMs} `;
+
+ // Add a local result.
+ let localValue = searchString + " local result";
+ await updateSearchHistory("bump", localValue);
+
+ // Do a search. The remote fetch should time out but the local result should
+ // be returned.
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch(searchString, false, getEngine);
+ Assert.equal(result.term, searchString);
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, localValue);
+ Assert.equal(result.remote.length, 0);
+
+ // The remote fetch isn't done yet, so the latency histogram should not be
+ // updated.
+ assertLatencyHistogram(histogram, false);
+
+ // Wait for the remote fetch to finish.
+ await new Promise(r => setTimeout(r, delayMs));
+
+ // Now the latency histogram should be updated.
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function slow_timeout_2() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ // Make the server return suggestions on a delay longer the timeout of the
+ // suggestion controller.
+ let delayMs = 3 * SearchSuggestionController.REMOTE_TIMEOUT_DEFAULT;
+ let searchString = `delay${delayMs} `;
+
+ // Add a local result.
+ let localValue = searchString + " local result";
+ await updateSearchHistory("bump", localValue);
+
+ // Do two searches using the same controller. Both times, the remote fetches
+ // should time out and only the local result should be returned. The second
+ // search should abort the remote fetch of the first search, and the remote
+ // fetch of the second search should be ongoing when the second search
+ // finishes.
+ let controller = new SearchSuggestionController();
+ for (let i = 0; i < 2; i++) {
+ let result = await controller.fetch(searchString, false, getEngine);
+ Assert.equal(result.term, searchString);
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, localValue);
+ Assert.equal(result.remote.length, 0);
+ }
+
+ // The remote fetch of the second search isn't done yet, so the latency
+ // histogram should not be updated.
+ assertLatencyHistogram(histogram, false);
+
+ // Wait for the second remote fetch to finish.
+ await new Promise(r => setTimeout(r, delayMs));
+
+ // Now the latency histogram should be updated, and only the remote fetch of
+ // the second search should be recorded.
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function slow_stop() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ // Make the server return suggestions on a delay longer the timeout of the
+ // suggestion controller.
+ let delayMs = 3 * SearchSuggestionController.REMOTE_TIMEOUT_DEFAULT;
+ let searchString = `delay${delayMs} `;
+
+ // Do a search but stop it before it finishes. Wait a tick before stopping it
+ // to better simulate the real world.
+ let controller = new SearchSuggestionController();
+ let resultPromise = controller.fetch(searchString, false, getEngine);
+ await TestUtils.waitForTick();
+ controller.stop();
+ let result = await resultPromise;
+ Assert.equal(result, null, "No result should be returned");
+
+ // The remote fetch should have been aborted by stopping the controller, but
+ // wait for the timeout period just to make sure it's done.
+ await new Promise(r => setTimeout(r, delayMs));
+
+ // Since the latencies of aborted fetches are not recorded, the latency
+ // histogram should not be updated.
+ assertLatencyHistogram(histogram, false);
+});
+
+// Error handling
+
+add_task(async function remote_term_mismatch() {
+ await updateSearchHistory("bump", "Query Mismatch Entry");
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("Query Mismatch", false, getEngine);
+ Assert.equal(result.term, "Query Mismatch");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "Query Mismatch Entry");
+ Assert.equal(result.remote.length, 0);
+
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function http_404() {
+ await updateSearchHistory("bump", "HTTP 404 Entry");
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("HTTP 404", false, getEngine);
+ Assert.equal(result.term, "HTTP 404");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "HTTP 404 Entry");
+ Assert.equal(result.remote.length, 0);
+
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function http_500() {
+ await updateSearchHistory("bump", "HTTP 500 Entry");
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("HTTP 500", false, getEngine);
+ Assert.equal(result.term, "HTTP 500");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "HTTP 500 Entry");
+ Assert.equal(result.remote.length, 0);
+
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function unresolvable_server() {
+ await updateSearchHistory("bump", "Unresolvable Server Entry");
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch(
+ "Unresolvable Server",
+ false,
+ unresolvableEngine
+ );
+ Assert.equal(result.term, "Unresolvable Server");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "Unresolvable Server Entry");
+ Assert.equal(result.remote.length, 0);
+
+ // This latency assert fails on Windows 7 (NT version 6.1), so skip it there.
+ if (!AppConstants.isPlatformAndVersionAtMost("win", "6.1")) {
+ assertLatencyHistogram(histogram, true);
+ }
+});
+
+// Exception handling
+
+add_task(async function missing_pb() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.fetch("No privacy");
+ }, /priva/i);
+});
+
+add_task(async function missing_engine() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.fetch("No engine", false);
+ }, /engine/i);
+});
+
+add_task(async function invalid_engine() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.fetch("invalid engine", false, {});
+ }, /engine/i);
+});
+
+add_task(async function no_results_requested() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 0;
+ controller.maxRemoteResults = 0;
+ controller.fetch("No results requested", false, getEngine);
+ }, /result/i);
+});
+
+add_task(async function minus_one_results_requested() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = -1;
+ controller.fetch("-1 results requested", false, getEngine);
+ }, /result/i);
+});
+
+add_task(async function test_userContextId() {
+ let controller = new SearchSuggestionController();
+ controller._fetchRemote = function (
+ searchTerm,
+ engine,
+ privateMode,
+ userContextId
+ ) {
+ Assert.equal(userContextId, 1);
+ return PromiseUtils.defer();
+ };
+
+ controller.fetch("test", false, getEngine, 1);
+});
+
+// Non-English characters
+
+add_task(async function suggestions_contain_escaped_unicode() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("stü", false, getEngine);
+ Assert.equal(result.term, "stü");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 2);
+ Assert.equal(result.remote[0].value, "stühle");
+ Assert.equal(result.remote[1].value, "stüssy");
+});
+
+// Helpers
+
+function updateSearchHistory(operation, value) {
+ return FormHistory.update({
+ op: operation,
+ fieldname: "searchbar-history",
+ value,
+ });
+}
+
+function assertLatencyHistogram(histogram, shouldRecord) {
+ let snapshot = histogram.snapshot();
+ info("Checking latency snapshot: " + JSON.stringify(snapshot));
+
+ // Build a map from engine ID => number of non-zero values recorded for it.
+ let valueCountByEngineId = Object.entries(snapshot).reduce(
+ (memo, [key, data]) => {
+ memo[key] = Object.values(data.values).filter(v => v != 0);
+ return memo;
+ },
+ {}
+ );
+
+ let expected = shouldRecord ? { [ENGINE_NAME]: [1] } : {};
+ Assert.deepEqual(
+ valueCountByEngineId,
+ expected,
+ shouldRecord ? "Latency histogram updated" : "Latency histogram not updated"
+ );
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_searchSuggest_cookies.js b/toolkit/components/search/tests/xpcshell/test_searchSuggest_cookies.js
new file mode 100644
index 0000000000..78d4fde8f1
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_searchSuggest_cookies.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that search suggestions from SearchSuggestionController.jsm don't store
+ * cookies.
+ */
+
+"use strict";
+
+const { SearchSuggestionController } = ChromeUtils.importESModule(
+ "resource://gre/modules/SearchSuggestionController.sys.mjs"
+);
+
+// We must make sure the FormHistoryStartup component is
+// initialized in order for it to respond to FormHistory
+// requests from nsFormAutoComplete.js.
+var formHistoryStartup = Cc[
+ "@mozilla.org/satchel/form-history-startup;1"
+].getService(Ci.nsIObserver);
+formHistoryStartup.observe(null, "profile-after-change", null);
+
+function countCacheEntries() {
+ info("Enumerating cache entries");
+ return new Promise(resolve => {
+ let storage = Services.cache2.diskCacheStorage(
+ Services.loadContextInfo.default
+ );
+ storage.asyncVisitStorage(
+ {
+ onCacheStorageInfo(num, consumption) {
+ this._num = num;
+ },
+ onCacheEntryInfo(uri) {
+ info("Found cache entry: " + uri.asciiSpec);
+ },
+ onCacheEntryVisitCompleted() {
+ resolve(this._num || 0);
+ },
+ },
+ true /* Do walk entries */
+ );
+ });
+}
+
+function countCookieEntries() {
+ info("Enumerating cookies");
+ let cookies = Services.cookies.cookies;
+ let cookieCount = 0;
+ for (let cookie of cookies) {
+ info(
+ "Cookie:" + cookie.rawHost + " " + JSON.stringify(cookie.originAttributes)
+ );
+ cookieCount++;
+ break;
+ }
+ return cookieCount;
+}
+
+let engines;
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+ Services.prefs.setBoolPref("browser.search.suggest.enabled.private", true);
+
+ registerCleanupFunction(async () => {
+ // Clean up all the data.
+ await new Promise(resolve =>
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve)
+ );
+ Services.prefs.clearUserPref("browser.search.suggest.enabled");
+ Services.prefs.clearUserPref("browser.search.suggest.enabled.private");
+ });
+
+ let server = useHttpServer();
+ server.registerContentType("sjs", "sjs");
+
+ let unicodeName = ["\u30a8", "\u30c9"].join("");
+ engines = [
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engineMaker.sjs?${JSON.stringify({
+ baseURL: gDataUrl,
+ name: unicodeName,
+ method: "GET",
+ })}`,
+ }),
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engineMaker.sjs?${JSON.stringify({
+ baseURL: gDataUrl,
+ name: "engine two",
+ method: "GET",
+ })}`,
+ }),
+ ];
+
+ // Clean up all the data.
+ await new Promise(resolve =>
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve)
+ );
+ Assert.equal(await countCacheEntries(), 0, "The cache should be empty");
+ Assert.equal(await countCookieEntries(), 0, "Should not find any cookie");
+});
+
+add_task(async function test_private_mode() {
+ await test_engine(true);
+});
+add_task(async function test_normal_mode() {
+ await test_engine(false);
+});
+
+async function test_engine(privateMode) {
+ info(`Testing ${privateMode ? "private" : "normal"} mode`);
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("no results", privateMode, engines[0]);
+ Assert.equal(result.local.length, 0, "Should have no local suggestions");
+ Assert.equal(result.remote.length, 0, "Should have no remote suggestions");
+
+ result = await controller.fetch("cookie", privateMode, engines[1]);
+ Assert.equal(result.local.length, 0, "Should have no local suggestions");
+ Assert.equal(result.remote.length, 0, "Should have no remote suggestions");
+ Assert.equal(await countCacheEntries(), 0, "The cache should be empty");
+ Assert.equal(await countCookieEntries(), 0, "Should not find any cookie");
+
+ let firstPartyDomain1 = controller.firstPartyDomains.get(engines[0].name);
+ Assert.ok(
+ /^[\.a-z0-9-]+\.search\.suggestions\.mozilla/.test(firstPartyDomain1),
+ "Check firstPartyDomain1"
+ );
+
+ let firstPartyDomain2 = controller.firstPartyDomains.get(engines[1].name);
+ Assert.ok(
+ /^[\.a-z0-9-]+\.search\.suggestions\.mozilla/.test(firstPartyDomain2),
+ "Check firstPartyDomain2"
+ );
+
+ Assert.notEqual(
+ firstPartyDomain1,
+ firstPartyDomain2,
+ "Check firstPartyDomain id unique per engine"
+ );
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_searchSuggest_extraParams.js b/toolkit/components/search/tests/xpcshell/test_searchSuggest_extraParams.js
new file mode 100644
index 0000000000..8bac7d39cb
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_searchSuggest_extraParams.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_CONFIG = [
+ {
+ webExtension: { id: "get@search.mozilla.org" },
+ appliesTo: [{ included: { everywhere: true } }],
+ suggestExtraParams: [
+ {
+ name: "custom_param",
+ pref: "test_pref_param",
+ condition: "pref",
+ },
+ ],
+ },
+];
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("method-extensions", null, TEST_CONFIG);
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+});
+
+add_task(async function test_custom_suggest_param() {
+ let engine = Services.search.getEngineByName("Get Engine");
+ Assert.notEqual(engine, null, "Should have found an engine");
+
+ let submissionSuggest = engine.getSubmission(
+ "bar",
+ SearchUtils.URL_TYPE.SUGGEST_JSON
+ );
+ Assert.equal(
+ submissionSuggest.uri.spec,
+ "https://example.com/?webExtension=1&suggest=bar",
+ "Suggest URLs should match"
+ );
+
+ let defaultBranch = Services.prefs.getDefaultBranch("browser.search.");
+ defaultBranch.setCharPref("param.test_pref_param", "good");
+
+ let nextSubmissionSuggest = engine.getSubmission(
+ "bar",
+ SearchUtils.URL_TYPE.SUGGEST_JSON
+ );
+ Assert.equal(
+ nextSubmissionSuggest.uri.spec,
+ "https://example.com/?custom_param=good&webExtension=1&suggest=bar",
+ "Suggest URLs should include custom param"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_searchSuggest_private.js b/toolkit/components/search/tests/xpcshell/test_searchSuggest_private.js
new file mode 100644
index 0000000000..3337bf9a27
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_searchSuggest_private.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that search suggestions from SearchSuggestionController.jsm operate
+ * correctly in private mode.
+ */
+
+"use strict";
+
+const { SearchSuggestionController } = ChromeUtils.importESModule(
+ "resource://gre/modules/SearchSuggestionController.sys.mjs"
+);
+
+let engine;
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+
+ let server = useHttpServer();
+ server.registerContentType("sjs", "sjs");
+
+ await AddonTestUtils.promiseStartupManager();
+
+ const engineData = {
+ baseURL: gDataUrl,
+ name: "GET suggestion engine",
+ method: "GET",
+ };
+
+ engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engineMaker.sjs?${JSON.stringify(engineData)}`,
+ });
+});
+
+add_task(async function test_suggestions_in_private_mode_enabled() {
+ Services.prefs.setBoolPref("browser.search.suggest.enabled.private", true);
+
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 0;
+ controller.maxRemoteResults = 1;
+ let result = await controller.fetch("mo", true, engine);
+ Assert.equal(result.remote.length, 1);
+});
+
+add_task(async function test_suggestions_in_private_mode_disabled() {
+ Services.prefs.setBoolPref("browser.search.suggest.enabled.private", false);
+
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 0;
+ controller.maxRemoteResults = 1;
+ let result = await controller.fetch("mo", true, engine);
+ Assert.equal(result.remote.length, 0);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_searchTermFromResult.js b/toolkit/components/search/tests/xpcshell/test_searchTermFromResult.js
new file mode 100644
index 0000000000..f2693efc3b
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_searchTermFromResult.js
@@ -0,0 +1,303 @@
+/* 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/. */
+
+/*
+ * Tests searchTermFromResult API.
+ */
+
+let defaultEngine;
+
+// The test string contains special characters to ensure
+// that they are encoded/decoded properly.
+const TERM = "c;,?:@&=+$-_.!~*'()# d\u00E8f";
+const TERM_ENCODED = "c%3B%2C%3F%3A%40%26%3D%2B%24-_.!~*'()%23+d%C3%A8f";
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("data", null, [
+ {
+ webExtension: {
+ id: "engine-purposes@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ default: "yes",
+ },
+ ],
+ },
+ ]);
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+
+ defaultEngine = Services.search.getEngineByName("Test Engine With Purposes");
+});
+
+add_task(async function test_searchTermFromResult_withAllPurposes() {
+ for (let purpose of Object.values(SearchUtils.PARAM_PURPOSES)) {
+ let uri = defaultEngine.getSubmission(TERM, null, purpose).uri;
+ let searchTerm = defaultEngine.searchTermFromResult(uri);
+ Assert.equal(
+ searchTerm,
+ TERM,
+ `Should return the correct url for purpose: ${purpose}`
+ );
+ }
+});
+
+add_task(async function test_searchTermFromResult() {
+ // Internationalized Domain Name search engine.
+ await SearchTestUtils.installSearchExtension({
+ name: "idn_addParam",
+ keyword: "idn_addParam",
+ search_url: "https://www.xn--bcher-kva.ch/search",
+ });
+ let engineEscapedIDN = Services.search.getEngineByName("idn_addParam");
+
+ // Setup server for french engine.
+ await useHttpServer();
+
+ // For ISO-8859-1 encoding testing.
+ let engineISOCharset = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engine-fr.xml`,
+ });
+
+ // For Windows-1252 encoding testing.
+ await SearchTestUtils.installSearchExtension({
+ name: "bacon_addParam",
+ keyword: "bacon_addParam",
+ encoding: "windows-1252",
+ search_url: "https://www.bacon.test/find",
+ });
+ let engineWinCharset = Services.search.getEngineByName("bacon_addParam");
+
+ // Verify getValidEngineUrl returns a URL that can return a search term.
+ let testUrl = getValidEngineUrl();
+ Assert.equal(
+ getTerm(testUrl),
+ TERM,
+ "Should get term from a url generated by getSubmission."
+ );
+
+ testUrl = getValidEngineUrl();
+ testUrl.pathname = "/SEARCH";
+ Assert.equal(
+ getTerm(testUrl),
+ TERM,
+ "Should get term even if path is not the same case as the engine."
+ );
+
+ let url = `https://www.xn--bcher-kva.ch/search?q=${TERM_ENCODED}`;
+ Assert.equal(
+ getTerm(url, engineEscapedIDN),
+ TERM,
+ "Should get term from IDNs urls."
+ );
+
+ url = `http://www.google.fr/search?q=caf%E8+au+lait&ie=iso-8859-1&oe=iso-8859-1`;
+ Assert.equal(
+ getTerm(url, engineISOCharset),
+ "caf\u00E8 au lait",
+ "Should get term from ISO-8859-1 encoded url containing a search term."
+ );
+
+ url = `http://www.google.fr/search?&ie=iso-8859-1&oe=iso-8859-1&q=`;
+ Assert.equal(
+ getTerm(url, engineISOCharset),
+ "",
+ "Should get a blank string from ISO-8859-1 encoded url missing a search term"
+ );
+
+ url = "https://www.bacon.test/find?q=caf%E8+au+lait";
+ Assert.equal(
+ getTerm(url, engineWinCharset),
+ "caf\u00E8 au lait",
+ "Should get term from Windows-1252 encoded url containing a search term."
+ );
+
+ url = "https://www.bacon.test/find?q=";
+ Assert.equal(
+ getTerm(url, engineWinCharset),
+ "",
+ "Should get a blank string from Windows-1252 encoded url missing a search term."
+ );
+
+ url = "about:blank";
+ Assert.equal(getTerm(url), "", "Should get a blank string from about:blank.");
+
+ url = "about:newtab";
+ Assert.equal(
+ getTerm(url),
+ "",
+ "Should get a blank string from about:newtab."
+ );
+});
+
+// Use a version of the url that should return a term and make minute
+// modifications that should cause it to return a blank value.
+add_task(async function test_searchTermFromResult_blank() {
+ let url = getValidEngineUrl();
+ url.searchParams.set("hello", "world");
+ Assert.equal(
+ getTerm(url),
+ "",
+ "Should get a blank string from url containing query param name not recognized by the engine."
+ );
+
+ url = getValidEngineUrl();
+ url.protocol = "http";
+ Assert.equal(
+ getTerm(url),
+ "",
+ "Should get a blank string from url that has a different scheme from the engine."
+ );
+
+ url = getValidEngineUrl();
+ url.protocol = "http";
+ Assert.equal(
+ getTerm(url),
+ "",
+ "Should get a blank string from url that has a different path from the engine."
+ );
+
+ url = getValidEngineUrl();
+ url.host = "images.example.com";
+ Assert.equal(
+ getTerm(url),
+ "",
+ "Should get a blank string from url that has a different host from the engine."
+ );
+
+ url = getValidEngineUrl();
+ url.host = "example.com";
+ Assert.equal(
+ getTerm(url),
+ "",
+ "Should get a blank string from url that has a different host from the engine."
+ );
+
+ url = getValidEngineUrl();
+ url.searchParams.set("form", "MOZUNKNOWN");
+ Assert.equal(
+ getTerm(url),
+ "",
+ "Should get a blank string from a url that has an un-recognized form value."
+ );
+
+ url = getValidEngineUrl();
+ url.searchParams.set("q", "");
+ Assert.equal(
+ getTerm(url),
+ "",
+ "Should get a blank string from a url with a missing search query value."
+ );
+
+ url = getValidEngineUrl();
+ url.searchParams.delete("q");
+ Assert.equal(
+ getTerm(url),
+ "",
+ "Should get a blank string from a url with a missing search query name."
+ );
+
+ url = getValidEngineUrl();
+ url.searchParams.delete("pc");
+ Assert.equal(
+ getTerm(url),
+ "",
+ "Should get a blank string from a url with a missing a query parameter."
+ );
+
+ url = getValidEngineUrl();
+ url.searchParams.delete("form");
+ Assert.equal(
+ getTerm(url),
+ "",
+ "Should get a blank string from a url with a missing a query parameter."
+ );
+});
+
+add_task(async function test_searchTermFromResult_prefParam() {
+ const defaultBranch = Services.prefs.getDefaultBranch(
+ SearchUtils.BROWSER_SEARCH_PREF
+ );
+
+ defaultBranch.setCharPref("param.testChannelEnabled", "yes");
+
+ let url = getValidEngineUrl(true);
+ Assert.equal(getTerm(url), TERM, "Should get term after pref is turned on.");
+
+ url.searchParams.delete("channel");
+ Assert.equal(
+ getTerm(url),
+ "",
+ "Should get a blank string if pref is on and channel param is missing."
+ );
+
+ defaultBranch.setCharPref("param.testChannelEnabled", "");
+ url = getValidEngineUrl(true);
+ Assert.equal(getTerm(url), TERM, "Should get term after pref is turned off.");
+
+ url.searchParams.set("channel", "yes");
+ Assert.equal(
+ getTerm(url),
+ "",
+ "Should get a blank string if pref is turned off but channel param is present."
+ );
+});
+
+// searchTermFromResult attempts to look into the template of a search
+// engine if query params aren't present in the url.params, so make sure
+// it works properly and fails gracefully.
+add_task(async function test_searchTermFromResult_paramsInSearchUrl() {
+ await SearchTestUtils.installSearchExtension({
+ name: "engine_params_in_search_url",
+ search_url: "https://example.com/?q={searchTerms}&pc=firefox",
+ search_url_get_params: "",
+ });
+ let testEngine = Services.search.getEngineByName(
+ "engine_params_in_search_url"
+ );
+ let url = `https://example.com/?q=${TERM_ENCODED}&pc=firefox`;
+ Assert.equal(
+ getTerm(url, testEngine),
+ TERM,
+ "Should get term from an engine with params in its search url."
+ );
+
+ url = `https://example.com/?q=${TERM_ENCODED}`;
+ Assert.equal(
+ getTerm(url, testEngine),
+ "",
+ "Should get a blank string when not all params are present."
+ );
+
+ await SearchTestUtils.installSearchExtension({
+ name: "engine_params_in_search_url_without_delimiter",
+ search_url: "https://example.com/q={searchTerms}",
+ search_url_get_params: "",
+ });
+ testEngine = Services.search.getEngineByName(
+ "engine_params_in_search_url_without_delimiter"
+ );
+ url = `https://example.com/?q=${TERM_ENCODED}&pc=firefox&page=1`;
+ Assert.equal(
+ getTerm(url, testEngine),
+ "",
+ "Should get a blank string from an engine with no params and no delimiter in its url."
+ );
+});
+
+function getTerm(url, searchEngine = defaultEngine) {
+ return searchEngine.searchTermFromResult(Services.io.newURI(url.toString()));
+}
+
+// Return a new instance of a submission URL so that it can modified
+// and tested again. Allow callers to force the cache to update, especially
+// if the engine is expected to have updated.
+function getValidEngineUrl(updateCache = false) {
+ if (updateCache || !this._submissionUrl) {
+ this._submissionUrl = defaultEngine.getSubmission(TERM, null).uri.spec;
+ }
+ return new URL(this._submissionUrl);
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_searchUrlDomain.js b/toolkit/components/search/tests/xpcshell/test_searchUrlDomain.js
new file mode 100644
index 0000000000..6cec77043a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_searchUrlDomain.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests searchUrlDomain API.
+ */
+
+"use strict";
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("data", null);
+ await AddonTestUtils.promiseStartupManager();
+});
+
+add_task(async function test_resultDomain() {
+ await Services.search.init();
+
+ let engine = Services.search.getEngineByName("Test search engine");
+
+ Assert.equal(engine.searchUrlDomain, "www.google.com");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_selectedEngine.js b/toolkit/components/search/tests/xpcshell/test_selectedEngine.js
new file mode 100644
index 0000000000..4a5d42211a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_selectedEngine.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const kDefaultEngineName = "engine1";
+
+add_task(async function setup() {
+ useHttpServer();
+ await AddonTestUtils.promiseStartupManager();
+ await SearchTestUtils.useTestEngines("data1");
+ Assert.ok(!Services.search.isInitialized);
+ Services.prefs.setBoolPref(
+ "browser.search.removeEngineInfobar.enabled",
+ false
+ );
+});
+
+// Check that the default engine matches the defaultenginename pref
+add_task(async function test_defaultEngine() {
+ await Services.search.init();
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engine.xml`,
+ });
+
+ Assert.equal(Services.search.defaultEngine.name, kDefaultEngineName);
+});
+
+// Setting the search engine should be persisted across restarts.
+add_task(async function test_persistAcrossRestarts() {
+ // Set the engine through the API.
+ await Services.search.setDefault(
+ Services.search.getEngineByName(kTestEngineName),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ Assert.equal(Services.search.defaultEngine.name, kTestEngineName);
+ await promiseAfterSettings();
+
+ // Check that the a hash was saved.
+ let metadata = await promiseGlobalMetadata();
+ Assert.equal(metadata.defaultEngineIdHash.length, 44);
+
+ // Re-init and check the engine is still the same.
+ Services.search.wrappedJSObject.reset();
+ await Services.search.init(true);
+ Assert.equal(Services.search.defaultEngine.name, kTestEngineName);
+
+ // Cleanup (set the engine back to default).
+ Services.search.resetToAppDefaultEngine();
+ Assert.equal(Services.search.defaultEngine.name, kDefaultEngineName);
+});
+
+// An engine set without a valid hash should be ignored.
+add_task(async function test_ignoreInvalidHash() {
+ // Set the engine through the API.
+ await Services.search.setDefault(
+ Services.search.getEngineByName(kTestEngineName),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ Assert.equal(Services.search.defaultEngine.name, kTestEngineName);
+ await promiseAfterSettings();
+
+ // Then mess with the file (make the hash invalid).
+ let metadata = await promiseGlobalMetadata();
+ metadata.defaultEngineIdHash = "invalid";
+ await promiseSaveGlobalMetadata(metadata);
+
+ // Re-init the search service, and check that the json file is ignored.
+ Services.search.wrappedJSObject.reset();
+ await Services.search.init(true);
+ Assert.equal(Services.search.defaultEngine.name, kDefaultEngineName);
+});
+
+// Resetting the engine to the default should remove the saved value.
+add_task(async function test_settingToDefault() {
+ // Set the engine through the API.
+ await Services.search.setDefault(
+ Services.search.getEngineByName(kTestEngineName),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ Assert.equal(Services.search.defaultEngine.name, kTestEngineName);
+ await promiseAfterSettings();
+
+ // Check that the current engine was saved.
+ let metadata = await promiseGlobalMetadata();
+ let currentEngine = Services.search.getEngineByName(kTestEngineName);
+ Assert.equal(metadata.defaultEngineId, currentEngine.id);
+
+ // Then set the engine back to the default through the API.
+ await Services.search.setDefault(
+ Services.search.getEngineByName(kDefaultEngineName),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await promiseAfterSettings();
+
+ // Check that the current engine is no longer saved in the JSON file.
+ metadata = await promiseGlobalMetadata();
+ Assert.equal(metadata.defaultEngineId, "");
+});
+
+add_task(async function test_resetToOriginalDefaultEngine() {
+ Assert.equal(Services.search.defaultEngine.name, kDefaultEngineName);
+
+ await Services.search.setDefault(
+ Services.search.getEngineByName(kTestEngineName),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ Assert.equal(Services.search.defaultEngine.name, kTestEngineName);
+ await promiseAfterSettings();
+
+ Services.search.resetToAppDefaultEngine();
+ Assert.equal(Services.search.defaultEngine.name, kDefaultEngineName);
+ await promiseAfterSettings();
+});
+
+add_task(async function test_fallback_kept_after_restart() {
+ // Set current engine to a default engine that isn't the original default.
+ let builtInEngines = await Services.search.getAppProvidedEngines();
+ let nonDefaultBuiltInEngine;
+ for (let engine of builtInEngines) {
+ if (engine.name != kDefaultEngineName) {
+ nonDefaultBuiltInEngine = engine;
+ break;
+ }
+ }
+ await Services.search.setDefault(
+ nonDefaultBuiltInEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ nonDefaultBuiltInEngine.name
+ );
+ await promiseAfterSettings();
+
+ // Remove that engine...
+ await Services.search.removeEngine(nonDefaultBuiltInEngine);
+ // The engine being a default (built-in) one, it should be hidden
+ // rather than actually removed.
+ Assert.ok(nonDefaultBuiltInEngine.hidden);
+
+ // Using the defaultEngine getter should force a fallback to the
+ // original default engine.
+ Assert.equal(Services.search.defaultEngine.name, kDefaultEngineName);
+
+ // Restoring the default engines should unhide our built-in test
+ // engine, but not change the value of defaultEngine.
+ Services.search.restoreDefaultEngines();
+ Assert.ok(!nonDefaultBuiltInEngine.hidden);
+ Assert.equal(Services.search.defaultEngine.name, kDefaultEngineName);
+ await promiseAfterSettings();
+
+ // After a restart, the defaultEngine value should still be unchanged.
+ Services.search.wrappedJSObject.reset();
+ await Services.search.init(true);
+ Assert.equal(Services.search.defaultEngine.name, kDefaultEngineName);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_sendSubmissionURL.js b/toolkit/components/search/tests/xpcshell/test_sendSubmissionURL.js
new file mode 100644
index 0000000000..26052c1bea
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sendSubmissionURL.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests covering sending submission URLs for major engines
+ */
+
+const SUBMISSION_YES = [
+ ["Google1 Test", "https://www.google.com/search", "q={searchTerms}"],
+ ["Google2 Test", "https://www.google.co.uk/search", "q={searchTerms}"],
+ ["Yahoo1 Test", "https://search.yahoo.com/search", "p={searchTerms}"],
+ ["Yahoo2 Test", "https://uk.search.yahoo.com/search", "p={searchTerms}"],
+ ["AOL1 Test", "https://search.aol.com/aol/search", "q={searchTerms}"],
+ ["AOL2 Test", "https://search.aol.co.uk/aol/search", "q={searchTerms}"],
+ ["Yandex1 Test", "https://yandex.ru/search/", "text={searchTerms}"],
+ ["Yandex2 Test", "https://yandex.com/search/", "text={searchTerms}"],
+ ["Ask1 Test", "https://www.ask.com/web", "q={searchTerms}"],
+ ["Ask2 Test", "https://fr.ask.com/web", "q={searchTerms}"],
+ ["Bing Test", "https://www.bing.com/search", "q={searchTerms}"],
+ [
+ "Startpage Test",
+ "https://www.startpage.com/do/search",
+ "query={searchTerms}",
+ ],
+ ["DuckDuckGo Test", "https://duckduckgo.com/", "q={searchTerms}"],
+ ["Baidu Test", "https://www.baidu.com/s", "wd={searchTerms}"],
+];
+
+const SUBMISSION_NO = [
+ ["Other1 Test", "https://example.com", "q={searchTerms}"],
+ ["Other2 Test", "https://googlebutnotgoogle.com", "q={searchTerms}"],
+];
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("data1");
+ await AddonTestUtils.promiseStartupManager();
+});
+
+async function addAndMakeDefault(name, search_url, search_url_get_params) {
+ await SearchTestUtils.installSearchExtension({
+ name,
+ search_url,
+ search_url_get_params,
+ });
+
+ let engine = Services.search.getEngineByName(name);
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ return engine;
+}
+
+add_task(async function test_submission_url_matching() {
+ Assert.ok(!Services.search.isInitialized);
+ let engineInfo;
+ let engine;
+
+ for (let [name, searchURL, searchParams] of SUBMISSION_YES) {
+ engine = await addAndMakeDefault(name, searchURL, searchParams);
+ engineInfo = Services.search.getDefaultEngineInfo();
+ Assert.equal(
+ engineInfo.defaultSearchEngineData.submissionURL,
+ (searchURL + "?" + searchParams).replace("{searchTerms}", "")
+ );
+ await Services.search.removeEngine(engine);
+ }
+
+ for (let [name, searchURL, searchParams] of SUBMISSION_NO) {
+ engine = await addAndMakeDefault(name, searchURL, searchParams);
+ engineInfo = Services.search.getDefaultEngineInfo();
+ Assert.equal(engineInfo.defaultSearchEngineData.submissionURL, null);
+ await Services.search.removeEngine(engine);
+ }
+});
+
+add_task(async function test_submission_url_built_in() {
+ const engine = await Services.search.getEngineByName("engine1");
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ const engineInfo = Services.search.getDefaultEngineInfo();
+ Assert.equal(
+ engineInfo.defaultSearchEngineData.submissionURL,
+ "https://1.example.com/search?q=",
+ "Should have given the submission url for a built-in engine."
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_settings.js b/toolkit/components/search/tests/xpcshell/test_settings.js
new file mode 100644
index 0000000000..3be00f460e
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_settings.js
@@ -0,0 +1,616 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test initializing from the search settings.
+ */
+
+"use strict";
+
+const legacyUseSavedOrderPrefName =
+ SearchUtils.BROWSER_SEARCH_PREF + "useDBForOrder";
+
+var settingsTemplate;
+
+/**
+ * Test reading from search.json.mozlz4
+ */
+add_task(async function setup() {
+ Services.prefs
+ .getDefaultBranch(SearchUtils.BROWSER_SEARCH_PREF + "param.")
+ .setCharPref("test", "expected");
+
+ await SearchTestUtils.useTestEngines("data1");
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+});
+
+async function loadSettingsFile(settingsFile, setVersion, setHashes) {
+ settingsTemplate = await readJSONFile(do_get_file(settingsFile));
+ if (setVersion) {
+ settingsTemplate.version = SearchUtils.SETTINGS_VERSION;
+ }
+
+ if (setHashes) {
+ settingsTemplate.metaData.hash = SearchUtils.getVerificationHash(
+ settingsTemplate.metaData.current
+ );
+ settingsTemplate.metaData.privateHash = SearchUtils.getVerificationHash(
+ settingsTemplate.metaData.private
+ );
+ }
+
+ delete settingsTemplate.visibleDefaultEngines;
+
+ await promiseSaveSettingsData(settingsTemplate);
+}
+
+/**
+ * Start the search service and confirm the engine properties match the expected values.
+ *
+ * @param {string} settingsFile
+ * The path to the settings file to use.
+ * @param {boolean} setVersion
+ * True if to set the version in the copied settings file.
+ * @param {boolean} expectedUseDBValue
+ * The value expected for the `useSavedOrder` metadata attribute.
+ */
+async function checkLoadSettingProperties(
+ settingsFile,
+ setVersion,
+ expectedUseDBValue
+) {
+ info("init search service");
+ let ss = Services.search.wrappedJSObject;
+
+ await loadSettingsFile(settingsFile, setVersion);
+
+ const settingsFileWritten = promiseAfterSettings();
+
+ await ss.reset();
+ await Services.search.init();
+
+ await settingsFileWritten;
+
+ let engines = await ss.getEngines();
+
+ Assert.equal(
+ engines[0].name,
+ "engine1",
+ "Should have loaded the correct first engine"
+ );
+ Assert.equal(engines[0].alias, "testAlias", "Should have set the alias");
+ Assert.equal(engines[0].hidden, false, "Should have not hidden the engine");
+ Assert.equal(engines[0].id, "engine1@search.mozilla.orgdefault");
+
+ Assert.equal(
+ engines[1].name,
+ "engine2",
+ "Should have loaded the correct second engine"
+ );
+ Assert.equal(engines[1].alias, "", "Should have not set the alias");
+ Assert.equal(engines[1].hidden, true, "Should have hidden the engine");
+ Assert.equal(engines[1].id, "engine2@search.mozilla.orgdefault");
+
+ // The extra engine is the second in the list.
+ isSubObjectOf(EXPECTED_ENGINE.engine, engines[2]);
+ Assert.ok(engines[2].id, "test-addon-id@mozilla.orgdefault");
+
+ let engineFromSS = ss.getEngineByName(EXPECTED_ENGINE.engine.name);
+ Assert.ok(!!engineFromSS);
+ isSubObjectOf(EXPECTED_ENGINE.engine, engineFromSS);
+
+ Assert.equal(
+ engineFromSS.getSubmission("foo").uri.spec,
+ "http://www.google.com/search?q=foo",
+ "Should have the correct URL with no mozparams"
+ );
+
+ Assert.equal(
+ ss._settings.getMetaDataAttribute("useSavedOrder"),
+ expectedUseDBValue,
+ "Should have set the useSavedOrder metadata correctly."
+ );
+
+ let migratedSettingsFile = await promiseSettingsData();
+
+ Assert.equal(
+ migratedSettingsFile.engines[0].id,
+ "engine1@search.mozilla.orgdefault"
+ );
+
+ removeSettingsFile();
+}
+
+add_task(async function test_legacy_setting_engine_properties() {
+ Services.prefs.setBoolPref(legacyUseSavedOrderPrefName, true);
+
+ let legacySettings = await readJSONFile(
+ do_get_file("data/search-legacy.json")
+ );
+
+ // Assert the engine ids have not been migrated yet
+ for (let engine of legacySettings.engines) {
+ Assert.ok(!("id" in engine));
+ }
+ Assert.ok(!("defaultEngineId" in legacySettings.metaData));
+ Assert.ok(!("privateDefaultEngineId" in legacySettings.metaData));
+
+ await checkLoadSettingProperties("data/search-legacy.json", false, true);
+
+ Assert.ok(
+ !Services.prefs.prefHasUserValue(legacyUseSavedOrderPrefName),
+ "Should have cleared the legacy pref."
+ );
+});
+
+add_task(
+ async function test_legacy_setting_migration_with_undefined_metaData_current_and_private() {
+ let ss = Services.search.wrappedJSObject;
+
+ await loadSettingsFile("data/search-legacy.json", false);
+ const settingsFileWritten = promiseAfterSettings();
+
+ await ss.reset();
+ await Services.search.init();
+
+ await settingsFileWritten;
+
+ let migratedSettingsFile = await promiseSettingsData();
+
+ Assert.equal(
+ migratedSettingsFile.metaData.defaultEngineId,
+ "",
+ "When there is no metaData.current attribute in settings file, the migration should set the defaultEngineId to an empty string."
+ );
+ Assert.equal(
+ migratedSettingsFile.metaData.privateDefaultEngineId,
+ "",
+ "When there is no metaData.private attribute in settings file, the migration should set the privateDefaultEngineId to an empty string."
+ );
+
+ removeSettingsFile();
+ }
+);
+
+add_task(
+ async function test_legacy_setting_migration_with_correct_metaData_current_and_private_hashes() {
+ let ss = Services.search.wrappedJSObject;
+
+ await loadSettingsFile(
+ "data/search-legacy-correct-default-engine-hashes.json",
+ false,
+ true
+ );
+ const settingsFileWritten = promiseAfterSettings();
+
+ await ss.reset();
+ await Services.search.init();
+
+ await settingsFileWritten;
+
+ let migratedSettingsFile = await promiseSettingsData();
+
+ Assert.equal(
+ migratedSettingsFile.metaData.defaultEngineId,
+ "engine2@search.mozilla.orgdefault",
+ "When the metaData.current and associated hash are correct, the migration should set the defaultEngineId to the engine id."
+ );
+ Assert.equal(
+ migratedSettingsFile.metaData.privateDefaultEngineId,
+ "engine2@search.mozilla.orgdefault",
+ "When the metaData.private and associated hash are correct, the migration should set the privateDefaultEngineId to the private engine id."
+ );
+
+ removeSettingsFile();
+ }
+);
+
+add_task(
+ async function test_legacy_setting_migration_with_incorrect_metaData_current_and_private_hashes_app_provided() {
+ let ss = Services.search.wrappedJSObject;
+
+ // Here we are testing correct migration for the case that a user has set
+ // their default engine to an application provided engine (but not the app
+ // default).
+ //
+ // In this case we should ignore invalid hashes for the default engines,
+ // and allow the select default to remain. This covers the case where
+ // a user has copied a profile from a different directory.
+ // See SearchService._getEngineDefault for more details.
+
+ await loadSettingsFile(
+ "data/search-legacy-wrong-default-engine-hashes.json",
+ false,
+ false
+ );
+ const settingsFileWritten = promiseAfterSettings();
+
+ await ss.reset();
+ await Services.search.init();
+
+ await settingsFileWritten;
+
+ let migratedSettingsFile = await promiseSettingsData();
+
+ Assert.equal(
+ migratedSettingsFile.metaData.defaultEngineId,
+ "engine2@search.mozilla.orgdefault",
+ "Should ignore invalid metaData.hash when the default engine is application provided."
+ );
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine2",
+ "Should have the correct engine set as default"
+ );
+
+ Assert.equal(
+ migratedSettingsFile.metaData.privateDefaultEngineId,
+ "engine2@search.mozilla.orgdefault",
+ "Should ignore invalid metaData.privateHash when the default private engine is application provided."
+ );
+ Assert.equal(
+ Services.search.defaultPrivateEngine.name,
+ "engine2",
+ "Should have the correct engine set as default private"
+ );
+
+ removeSettingsFile();
+ }
+);
+
+add_task(
+ async function test_legacy_setting_migration_with_incorrect_metaData_current_and_private_hashes_third_party() {
+ let ss = Services.search.wrappedJSObject;
+
+ // This test is checking that if the user has set a third-party engine as
+ // default, and the verification hash is invalid, then we do not copy
+ // the default engine setting.
+
+ await loadSettingsFile(
+ "data/search-legacy-wrong-third-party-engine-hashes.json",
+ false,
+ false
+ );
+ const settingsFileWritten = promiseAfterSettings();
+
+ await ss.reset();
+ await Services.search.init();
+
+ await settingsFileWritten;
+
+ let migratedSettingsFile = await promiseSettingsData();
+
+ Assert.equal(
+ migratedSettingsFile.metaData.defaultEngineId,
+ "",
+ "Should reset the default engine when metaData.hash is invalid and the engine is not application provided."
+ );
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine1",
+ "Should have reset the default engine"
+ );
+
+ Assert.equal(
+ migratedSettingsFile.metaData.privateDefaultEngineId,
+ "",
+ "Should reset the default engine when metaData.privateHash is invalid and the engine is not application provided."
+ );
+ Assert.equal(
+ Services.search.defaultPrivateEngine.name,
+ "engine1",
+ "Should have reset the default private engine"
+ );
+
+ removeSettingsFile();
+ }
+);
+
+add_task(async function test_current_setting_engine_properties() {
+ await checkLoadSettingProperties("data/search.json", true, false);
+});
+
+add_task(async function test_settings_metadata_properties() {
+ let ss = Services.search.wrappedJSObject;
+
+ await loadSettingsFile("data/search.json");
+
+ const settingsFileWritten = promiseAfterSettings();
+ await ss.reset();
+ await Services.search.init();
+
+ await settingsFileWritten;
+
+ let metaDataProperties = [
+ "locale",
+ "region",
+ "channel",
+ "experiment",
+ "distroID",
+ ];
+
+ for (let name of metaDataProperties) {
+ Assert.notEqual(
+ ss._settings.getMetaDataAttribute(`${name}`),
+ undefined,
+ `Search settings should have ${name} property defined.`
+ );
+ }
+
+ removeSettingsFile();
+});
+
+add_task(async function test_settings_write_when_settings_changed() {
+ let ss = Services.search.wrappedJSObject;
+ await loadSettingsFile("data/search.json");
+
+ const settingsFileWritten = promiseAfterSettings();
+ await ss.reset();
+ await Services.search.init();
+ await settingsFileWritten;
+
+ Assert.ok(
+ ss._settings.isCurrentAndCachedSettingsEqual(),
+ "Settings and cached settings should be the same after search service initializaiton."
+ );
+
+ const settingsFileWritten2 = promiseAfterSettings();
+ ss._settings.setMetaDataAttribute("value", "test");
+
+ Assert.ok(
+ !ss._settings.isCurrentAndCachedSettingsEqual(),
+ "Settings should differ from cached settings after a new attribute is set."
+ );
+
+ await settingsFileWritten2;
+ info("Settings write complete");
+
+ Assert.ok(
+ ss._settings.isCurrentAndCachedSettingsEqual(),
+ "Settings and cached settings should be the same after new attribte on settings is written."
+ );
+
+ removeSettingsFile();
+});
+
+add_task(async function test_set_and_get_engine_metadata_attribute() {
+ let ss = Services.search.wrappedJSObject;
+ await loadSettingsFile("data/search.json");
+
+ const settingsFileWritten = promiseAfterSettings();
+ await ss.reset();
+ await Services.search.init();
+ await settingsFileWritten;
+
+ let engines = await ss.getEngines();
+ const settingsFileWritten2 = promiseAfterSettings();
+ ss._settings.setEngineMetaDataAttribute(engines[0].name, "value", "test");
+ await settingsFileWritten2;
+
+ Assert.equal(
+ "test",
+ ss._settings.getEngineMetaDataAttribute(engines[0].name, "value"),
+ `${engines[0].name}'s metadata property "value" should be set as "test" after calling getEngineMetaDataAttribute.`
+ );
+
+ let userSettings = await ss._settings.get();
+ let engine = userSettings.engines.find(e => e._name == engines[0].name);
+
+ Assert.equal(
+ "test",
+ engine._metaData.value,
+ `${engines[0].name}'s metadata property "value" should be set as "test" from settings file.`
+ );
+
+ removeSettingsFile();
+});
+
+add_task(
+ async function test_settings_write_prevented_when_settings_unchanged() {
+ let ss = Services.search.wrappedJSObject;
+ await loadSettingsFile("data/search.json");
+
+ const settingsFileWritten = promiseAfterSettings();
+ await ss.reset();
+ await Services.search.init();
+ await settingsFileWritten;
+
+ Assert.ok(
+ ss._settings.isCurrentAndCachedSettingsEqual(),
+ "Settings and cached settings should be the same after search service initializaiton."
+ );
+
+ // Update settings.
+ const settingsFileWritten2 = promiseAfterSettings();
+ ss._settings.setMetaDataAttribute("value", "test");
+
+ Assert.ok(
+ !ss._settings.isCurrentAndCachedSettingsEqual(),
+ "Settings should differ from cached settings after a new attribute is set."
+ );
+ await settingsFileWritten2;
+
+ // Set the same attribute as before to ensure there was no change.
+ // Settings write should be prevented.
+ let promiseWritePrevented = SearchTestUtils.promiseSearchNotification(
+ "write-prevented-when-settings-unchanged"
+ );
+ ss._settings.setMetaDataAttribute("value", "test");
+
+ Assert.ok(
+ ss._settings.isCurrentAndCachedSettingsEqual(),
+ "Settings and cached settings should be the same."
+ );
+ await promiseWritePrevented;
+
+ removeSettingsFile();
+ }
+);
+
+/**
+ * Test that the JSON settings written in the profile is correct.
+ */
+add_task(async function test_settings_write() {
+ let ss = Services.search.wrappedJSObject;
+ info("test settings writing");
+
+ await loadSettingsFile("data/search.json");
+
+ const settingsFileWritten = promiseAfterSettings();
+ await ss.reset();
+ await Services.search.init();
+ await settingsFileWritten;
+
+ let settingsData = await promiseSettingsData();
+
+ // Remove buildID and locale, as they are no longer used.
+ delete settingsTemplate.buildID;
+ delete settingsTemplate.locale;
+
+ for (let engine of settingsTemplate.engines) {
+ // Remove _shortName from the settings template, as it is no longer supported,
+ // but older settings used to have it, so we keep it in the template as an
+ // example.
+ if ("_shortName" in engine) {
+ delete engine._shortName;
+ }
+ if ("_urls" in engine) {
+ // Only app-provided engines support purpose & mozparams, others do not,
+ // so filter them out of the expected template.
+ for (let urls of engine._urls) {
+ urls.params = urls.params.filter(p => !p.purpose && !p.mozparam);
+ // resultDomain is also no longer supported.
+ if ("resultDomain" in urls) {
+ delete urls.resultDomain;
+ }
+ }
+ }
+ // Remove queryCharset, if it is the same as the default, as we don't save
+ // it in that case.
+ if (engine?.queryCharset == SearchUtils.DEFAULT_QUERY_CHARSET) {
+ delete engine.queryCharset;
+ }
+ }
+
+ // Note: the file is copied with an old version number, which should have
+ // been updated on write.
+ settingsTemplate.version = SearchUtils.SETTINGS_VERSION;
+
+ isSubObjectOf(settingsTemplate, settingsData, (prop, value) => {
+ if (prop != "_iconURL" && prop != "{}") {
+ return false;
+ }
+ // Skip items that are to do with icons for extensions, as we can't
+ // control the uuid.
+ return value.startsWith("moz-extension://");
+ });
+});
+
+async function settings_write_check(disableFn) {
+ let ss = Services.search.wrappedJSObject;
+
+ sinon.stub(ss._settings, "_write").returns(Promise.resolve());
+
+ // Simulate the search service being initialized.
+ disableFn(true);
+
+ ss._settings.setMetaDataAttribute("value", "test");
+
+ Assert.ok(
+ ss._settings._write.notCalled,
+ "Should not have attempted to _write"
+ );
+
+ // Wait for two periods of the normal delay to ensure we still do not write.
+ await new Promise(r =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(r, SearchSettings.SETTNGS_INVALIDATION_DELAY * 2)
+ );
+
+ Assert.ok(
+ ss._settings._write.notCalled,
+ "Should not have attempted to _write"
+ );
+
+ disableFn(false);
+
+ await TestUtils.waitForCondition(
+ () => ss._settings._write.calledOnce,
+ "Should attempt to write the settings."
+ );
+
+ sinon.restore();
+}
+
+add_task(async function test_settings_write_prevented_during_init() {
+ await settings_write_check(disable => {
+ let status = disable ? "success" : "failed";
+ Services.search.wrappedJSObject.forceInitializationStatusForTests(status);
+ });
+});
+
+add_task(async function test_settings_write_prevented_during_reload() {
+ await settings_write_check(
+ disable => (Services.search.wrappedJSObject._reloadingEngines = disable)
+ );
+});
+
+var EXPECTED_ENGINE = {
+ engine: {
+ name: "Test search engine",
+ alias: "",
+ description: "A test search engine (based on Google search)",
+ searchForm: "http://www.google.com/",
+ wrappedJSObject: {
+ _extensionID: "test-addon-id@mozilla.org",
+ _iconURL:
+ "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAA" +
+ "AIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9F" +
+ "sfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2F" +
+ "Ptft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2Fgg" +
+ "M%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F" +
+ "%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJ" +
+ "vvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%" +
+ "2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%" +
+ "2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%" +
+ "2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%" +
+ "2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYS" +
+ "BHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWc" +
+ "TxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4j" +
+ "wA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsgg" +
+ "A7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7" +
+ "kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%" +
+ "2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFE" +
+ "MwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%" +
+ "2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCT" +
+ "IYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesA" +
+ "AN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOc" +
+ "AAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v" +
+ "8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA",
+ _urls: [
+ {
+ type: "application/x-suggestions+json",
+ method: "GET",
+ template:
+ "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox" +
+ "&hl={moz:locale}&q={searchTerms}",
+ params: "",
+ },
+ {
+ type: "text/html",
+ method: "GET",
+ template: "http://www.google.com/search",
+ params: [
+ {
+ name: "q",
+ value: "{searchTerms}",
+ purpose: undefined,
+ },
+ ],
+ },
+ ],
+ },
+ },
+};
diff --git a/toolkit/components/search/tests/xpcshell/test_settings_broken.js b/toolkit/components/search/tests/xpcshell/test_settings_broken.js
new file mode 100644
index 0000000000..12298155f1
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_settings_broken.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test initializing from broken search settings. This is one where the engines
+ * array for some reason has lost all the default engines, but retained either
+ * one or two, or a user-supplied engine. We don't know why this happens, but
+ * we have seen it (bug 1578807).
+ */
+
+"use strict";
+
+const { getAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+
+const enginesSettings = {
+ version: SearchUtils.SETTINGS_VERSION,
+ buildID: "TBD",
+ appVersion: "TBD",
+ locale: "en-US",
+ metaData: {
+ searchDefault: "Test search engine",
+ searchDefaultHash: "TBD",
+ // Intentionally in the past, but shouldn't actually matter for this test.
+ searchDefaultExpir: 1567694909002,
+ current: "",
+ hash: "TBD",
+ visibleDefaultEngines:
+ "engine,engine-pref,engine-rel-searchform-purpose,engine-chromeicon,engine-resourceicon,engine-reordered",
+ visibleDefaultEnginesHash: "TBD",
+ },
+ engines: [
+ // This is a user-installed engine - the only one that was listed due to the
+ // original issue.
+ {
+ _name: "A second test engine",
+ _shortName: "engine2",
+ _loadPath: "[profile]/searchplugins/engine2.xml",
+ description: "A second test search engine (based on DuckDuckGo)",
+ _iconURL:
+ "data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAEAgQAhIOEAMjHyABIR0gA6ejpAGlqaQCpqKkAKCgoAPz9/AAZGBkAmJiYANjZ2ABXWFcAent6ALm6uQA8OjwAiIiIiIiIiIiIiI4oiL6IiIiIgzuIV4iIiIhndo53KIiIiB/WvXoYiIiIfEZfWBSIiIEGi/foqoiIgzuL84i9iIjpGIoMiEHoiMkos3FojmiLlUipYliEWIF+iDe0GoRa7D6GPbjcu1yIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+ _iconMapObj: {
+ '{"width":16,"height":16}':
+ "data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAEAgQAhIOEAMjHyABIR0gA6ejpAGlqaQCpqKkAKCgoAPz9/AAZGBkAmJiYANjZ2ABXWFcAent6ALm6uQA8OjwAiIiIiIiIiIiIiI4oiL6IiIiIgzuIV4iIiIhndo53KIiIiB/WvXoYiIiIfEZfWBSIiIEGi/foqoiIgzuL84i9iIjpGIoMiEHoiMkos3FojmiLlUipYliEWIF+iDe0GoRa7D6GPbjcu1yIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+ },
+ _isBuiltin: false,
+ _metaData: {
+ order: 1,
+ },
+ _urls: [
+ {
+ template: "https://duckduckgo.com/?q={searchTerms}",
+ rels: [],
+ resultDomain: "duckduckgo.com",
+ params: [],
+ },
+ ],
+ queryCharset: "UTF-8",
+ filePath: "TBD",
+ },
+ ],
+};
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+
+ // Allow telemetry probes which may otherwise be disabled for some applications (e.g. Thunderbird)
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ await SearchTestUtils.useTestEngines();
+ Services.prefs.setCharPref(SearchUtils.BROWSER_SEARCH_PREF + "region", "US");
+ Services.locale.availableLocales = ["en-US"];
+ Services.locale.requestedLocales = ["en-US"];
+
+ // We dynamically generate the hashes because these depend on the profile.
+ enginesSettings.metaData.searchDefaultHash = SearchUtils.getVerificationHash(
+ enginesSettings.metaData.searchDefault
+ );
+ enginesSettings.metaData.hash = SearchUtils.getVerificationHash(
+ enginesSettings.metaData.current
+ );
+ enginesSettings.metaData.visibleDefaultEnginesHash =
+ SearchUtils.getVerificationHash(
+ enginesSettings.metaData.visibleDefaultEngines
+ );
+ const appInfo = getAppInfo();
+ enginesSettings.buildID = appInfo.platformBuildID;
+ enginesSettings.appVersion = appInfo.version;
+
+ await IOUtils.writeJSON(
+ PathUtils.join(PathUtils.profileDir, SETTINGS_FILENAME),
+ enginesSettings,
+ { compress: true }
+ );
+});
+
+add_task(async function test_cached_engine_properties() {
+ info("init search service");
+
+ const initResult = await Services.search.init();
+
+ info("init'd search service");
+ Assert.ok(
+ Components.isSuccessCode(initResult),
+ "Should have successfully created the search service"
+ );
+
+ const engines = await Services.search.getEngines();
+
+ const expectedEngines = [
+ // Default engines
+ "Test search engine",
+ // Rest of engines in order
+ "engine-resourceicon",
+ "engine-chromeicon",
+ "engine-pref",
+ "engine-rel-searchform-purpose",
+ "Test search engine (Reordered)",
+ "A second test engine",
+ ];
+
+ Assert.deepEqual(
+ engines.map(e => e.name),
+ expectedEngines,
+ "Should have the expected default engines"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_settings_duplicate.js b/toolkit/components/search/tests/xpcshell/test_settings_duplicate.js
new file mode 100644
index 0000000000..b269fcafca
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_settings_duplicate.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test initializing with an engine that's a duplicate of an app-provided
+ * engine.
+ */
+
+"use strict";
+
+const { getAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+
+const enginesSettings = {
+ version: SearchUtils.SETTINGS_VERSION,
+ buildID: "TBD",
+ appVersion: "TBD",
+ locale: "en-US",
+ metaData: {
+ searchDefault: "Test search engine",
+ searchDefaultHash: "TBD",
+ // Intentionally in the past, but shouldn't actually matter for this test.
+ searchDefaultExpir: 1567694909002,
+ current: "",
+ hash: "TBD",
+ visibleDefaultEngines:
+ "engine,engine-pref,engine-rel-searchform-purpose,engine-chromeicon,engine-resourceicon,engine-reordered",
+ visibleDefaultEnginesHash: "TBD",
+ },
+ engines: [
+ {
+ _metaData: { alias: null },
+ _isAppProvided: true,
+ _name: "engine1",
+ },
+ {
+ _metaData: { alias: null },
+ _isAppProvided: true,
+ _name: "engine2",
+ },
+ // This is a user-installed engine - the only one that was listed due to the
+ // original issue.
+ {
+ _name: "engine1",
+ _shortName: "engine1",
+ _loadPath: "[test]oldduplicateversion",
+ description: "An old near duplicate version of engine1",
+ _iconURL:
+ "data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAEAgQAhIOEAMjHyABIR0gA6ejpAGlqaQCpqKkAKCgoAPz9/AAZGBkAmJiYANjZ2ABXWFcAent6ALm6uQA8OjwAiIiIiIiIiIiIiI4oiL6IiIiIgzuIV4iIiIhndo53KIiIiB/WvXoYiIiIfEZfWBSIiIEGi/foqoiIgzuL84i9iIjpGIoMiEHoiMkos3FojmiLlUipYliEWIF+iDe0GoRa7D6GPbjcu1yIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+ _iconMapObj: {
+ '{"width":16,"height":16}':
+ "data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAEAgQAhIOEAMjHyABIR0gA6ejpAGlqaQCpqKkAKCgoAPz9/AAZGBkAmJiYANjZ2ABXWFcAent6ALm6uQA8OjwAiIiIiIiIiIiIiI4oiL6IiIiIgzuIV4iIiIhndo53KIiIiB/WvXoYiIiIfEZfWBSIiIEGi/foqoiIgzuL84i9iIjpGIoMiEHoiMkos3FojmiLlUipYliEWIF+iDe0GoRa7D6GPbjcu1yIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+ },
+ _metaData: {
+ order: 1,
+ },
+ _urls: [
+ {
+ template: "https://example.com/?myquery={searchTerms}",
+ rels: [],
+ resultDomain: "example.com",
+ params: [],
+ },
+ ],
+ queryCharset: "UTF-8",
+ filePath: "TBD",
+ },
+ ],
+};
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+
+ // Allow telemetry probes which may otherwise be disabled for some applications (e.g. Thunderbird)
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ await SearchTestUtils.useTestEngines("data1");
+ Services.prefs.setCharPref(SearchUtils.BROWSER_SEARCH_PREF + "region", "US");
+ Services.locale.availableLocales = ["en-US"];
+ Services.locale.requestedLocales = ["en-US"];
+
+ // We dynamically generate the hashes because these depend on the profile.
+ enginesSettings.metaData.searchDefaultHash = SearchUtils.getVerificationHash(
+ enginesSettings.metaData.searchDefault
+ );
+ enginesSettings.metaData.hash = SearchUtils.getVerificationHash(
+ enginesSettings.metaData.current
+ );
+ enginesSettings.metaData.visibleDefaultEnginesHash =
+ SearchUtils.getVerificationHash(
+ enginesSettings.metaData.visibleDefaultEngines
+ );
+ let appInfo = getAppInfo();
+ enginesSettings.buildID = appInfo.platformBuildID;
+ enginesSettings.appVersion = appInfo.version;
+
+ await IOUtils.write(
+ PathUtils.join(PathUtils.profileDir, SETTINGS_FILENAME),
+ new TextEncoder().encode(JSON.stringify(enginesSettings)),
+ { compress: true }
+ );
+});
+
+add_task(async function test_cached_duplicate() {
+ info("init search service");
+
+ let initResult = await Services.search.init();
+
+ info("init'd search service");
+ Assert.ok(
+ Components.isSuccessCode(initResult),
+ "Should have successfully created the search service"
+ );
+
+ let engine = await Services.search.getEngineByName("engine1");
+ let submission = engine.getSubmission("foo");
+ Assert.equal(
+ submission.uri.spec,
+ "https://1.example.com/search?q=foo",
+ "Should have not changed the app provided engine."
+ );
+
+ let engines = await Services.search.getEngines();
+
+ Assert.deepEqual(
+ engines.map(e => e.name),
+ ["engine1", "engine2"],
+ "Should have the expected default engines"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_settings_good.js b/toolkit/components/search/tests/xpcshell/test_settings_good.js
new file mode 100644
index 0000000000..2c69889c09
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_settings_good.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test initializing from good search settings.
+ */
+
+"use strict";
+
+const { getAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+
+const enginesSettings = {
+ version: SearchUtils.SETTINGS_VERSION,
+ buildID: "TBD",
+ appVersion: "TBD",
+ locale: "en-US",
+ metaData: {
+ searchDefault: "Test search engine",
+ searchDefaultHash: "TBD",
+ // Intentionally in the past, but shouldn't actually matter for this test.
+ searchDefaultExpir: 1567694909002,
+ // We use the second engine here so that the user's default is set
+ // to something different, and hence so that we exercise the appropriate
+ // code paths.
+ defaultEngineId: "engine2@search.mozilla.orgdefault",
+ defaultEngineIdHash: "TBD",
+ visibleDefaultEngines: "engine1,engine2",
+ visibleDefaultEnginesHash: "TBD",
+ },
+ engines: [
+ {
+ _metaData: { alias: null },
+ _isAppProvided: true,
+ _name: "engine1",
+ },
+ {
+ _metaData: { alias: null },
+ _isAppProvided: true,
+ _name: "engine2",
+ },
+ ],
+};
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+
+ // Allow telemetry probes which may otherwise be disabled for some applications (e.g. Thunderbird)
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ await SearchTestUtils.useTestEngines("data1");
+ Services.prefs.setCharPref(SearchUtils.BROWSER_SEARCH_PREF + "region", "US");
+ Services.locale.availableLocales = ["en-US"];
+ Services.locale.requestedLocales = ["en-US"];
+
+ // We dynamically generate the hashes because these depend on the profile.
+ enginesSettings.metaData.searchDefaultHash = SearchUtils.getVerificationHash(
+ enginesSettings.metaData.searchDefault
+ );
+ enginesSettings.metaData.defaultEngineIdHash =
+ SearchUtils.getVerificationHash(enginesSettings.metaData.defaultEngineId);
+ enginesSettings.metaData.visibleDefaultEnginesHash =
+ SearchUtils.getVerificationHash(
+ enginesSettings.metaData.visibleDefaultEngines
+ );
+ const appInfo = getAppInfo();
+ enginesSettings.buildID = appInfo.platformBuildID;
+ enginesSettings.appVersion = appInfo.version;
+
+ await IOUtils.writeJSON(
+ PathUtils.join(PathUtils.profileDir, SETTINGS_FILENAME),
+ enginesSettings,
+ { compress: true }
+ );
+});
+
+add_task(async function test_cached_engine_properties() {
+ info("init search service");
+
+ const initResult = await Services.search.init();
+
+ info("init'd search service");
+ Assert.ok(
+ Components.isSuccessCode(initResult),
+ "Should have successfully created the search service"
+ );
+
+ const engines = await Services.search.getEngines();
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "engine2",
+ "Should have the expected default engine"
+ );
+ Assert.deepEqual(
+ engines.map(e => e.name),
+ ["engine1", "engine2"],
+ "Should have the expected application provided engines"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_settings_ignorelist.js b/toolkit/components/search/tests/xpcshell/test_settings_ignorelist.js
new file mode 100644
index 0000000000..b02775bbd9
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_settings_ignorelist.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test initializing from the search settings.
+ */
+
+"use strict";
+
+var { getAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+
+var settingsTemplate;
+
+/**
+ * Test reading from search.json.mozlz4
+ */
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+
+ await setupRemoteSettings();
+
+ settingsTemplate = await readJSONFile(
+ do_get_file("data/search_ignorelist.json")
+ );
+ settingsTemplate.buildID = getAppInfo().platformBuildID;
+
+ await promiseSaveSettingsData(settingsTemplate);
+});
+
+/**
+ * Start the search service and confirm the settings were reset
+ */
+add_task(async function test_settings_rest() {
+ info("init search service");
+
+ let updatePromise = SearchTestUtils.promiseSearchNotification(
+ "settings-update-complete"
+ );
+
+ let result = await Services.search.init();
+
+ Assert.ok(
+ Components.isSuccessCode(result),
+ "Search service should be successfully initialized"
+ );
+ await updatePromise;
+
+ const engines = await Services.search.getEngines();
+
+ // Engine list will have been reset to the default,
+ // Not the one engine in the settings.
+ // It should have more than one engine.
+ Assert.greater(
+ engines.length,
+ 1,
+ "Should have more than one engine in the list"
+ );
+
+ removeSettingsFile();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_settings_migration_ids.js b/toolkit/components/search/tests/xpcshell/test_settings_migration_ids.js
new file mode 100644
index 0000000000..f5ccb8a301
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_settings_migration_ids.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test migration of user, enterprise policy and OpenSearch engines
+ * from when engines were referenced by name rather than id.
+ *
+ * Add-ons and default engine ids are tested in test_settings.js.
+ */
+
+"use strict";
+
+const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+);
+
+const enterprisePolicy = {
+ policies: {
+ SearchEngines: {
+ Add: [
+ {
+ Name: "Policy",
+ Encoding: "windows-1252",
+ URLTemplate: "http://example.com/?q={searchTerms}",
+ },
+ ],
+ },
+ },
+};
+
+/**
+ * Loads the settings file and ensures it has not already been migrated.
+ *
+ * @param {string} settingsFile The settings file to load
+ */
+async function loadSettingsFile(settingsFile) {
+ let settingsTemplate = await readJSONFile(do_get_file(settingsFile));
+
+ Assert.less(
+ settingsTemplate.version,
+ 7,
+ "Should be a version older than when indexing engines by id was introduced"
+ );
+ for (let engine of settingsTemplate.engines) {
+ Assert.ok(!("id" in engine));
+ }
+
+ await promiseSaveSettingsData(settingsTemplate);
+}
+
+/**
+ * Test reading from search.json.mozlz4
+ */
+add_task(async function setup() {
+ // This initializes the policy engine for xpcshell tests
+ let policies = Cc["@mozilla.org/enterprisepolicies;1"].getService(
+ Ci.nsIObserver
+ );
+ policies.observe(null, "policies-startup", null);
+
+ Services.prefs
+ .getDefaultBranch(SearchUtils.BROWSER_SEARCH_PREF + "param.")
+ .setCharPref("test", "expected");
+
+ await SearchTestUtils.useTestEngines("data1");
+ await AddonTestUtils.promiseStartupManager();
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson(enterprisePolicy);
+ // Setting the enterprise policy starts the search service initialising,
+ // so we wait for that to complete before starting the test.
+ await Services.search.init();
+});
+
+/**
+ * Tests that an installed engine matches the expected data.
+ *
+ * @param {object} expectedData The expected data for the engine
+ */
+async function assertInstalledEngineMatches(expectedData) {
+ let engine = await Services.search.getEngineByName(expectedData.name);
+
+ Assert.ok(engine, `Should have found the ${expectedData.type} engine`);
+ if (expectedData.idLength) {
+ Assert.equal(
+ engine.id.length,
+ expectedData.idLength,
+ "Should have been given an id"
+ );
+ } else {
+ Assert.equal(engine.id, expectedData.id, "Should have the expected id");
+ }
+ Assert.equal(engine.alias, expectedData.alias, "Should have kept the alias");
+}
+
+add_task(async function test_migration_from_pre_ids() {
+ await loadSettingsFile("data/search-legacy-no-ids.json");
+
+ const settingsFileWritten = promiseAfterSettings();
+
+ await Services.search.wrappedJSObject.reset();
+ await Services.search.init();
+
+ await settingsFileWritten;
+
+ await assertInstalledEngineMatches({
+ type: "OpenSearch",
+ name: "Bugzilla@Mozilla",
+ idLength: 36,
+ alias: "bugzillaAlias",
+ });
+ await assertInstalledEngineMatches({
+ type: "Enterprise Policy",
+ name: "Policy",
+ id: "policy-Policy",
+ alias: "PolicyAlias",
+ });
+ await assertInstalledEngineMatches({
+ type: "User",
+ name: "User",
+ idLength: 36,
+ alias: "UserAlias",
+ });
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_settings_migration_loadPath.js b/toolkit/components/search/tests/xpcshell/test_settings_migration_loadPath.js
new file mode 100644
index 0000000000..e6a7cbce00
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_settings_migration_loadPath.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test migration load path for user, enterprise policy and add-on
+ * engines.
+ */
+
+"use strict";
+
+const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+);
+
+const enterprisePolicy = {
+ policies: {
+ SearchEngines: {
+ Add: [
+ {
+ Name: "Policy",
+ Encoding: "windows-1252",
+ URLTemplate: "http://example.com/?q={searchTerms}",
+ },
+ ],
+ },
+ },
+};
+
+add_task(async function setup() {
+ // This initializes the policy engine for xpcshell tests
+ let policies = Cc["@mozilla.org/enterprisepolicies;1"].getService(
+ Ci.nsIObserver
+ );
+ policies.observe(null, "policies-startup", null);
+
+ Services.prefs
+ .getDefaultBranch(SearchUtils.BROWSER_SEARCH_PREF + "param.")
+ .setCharPref("test", "expected");
+
+ await SearchTestUtils.useTestEngines("data1");
+ await AddonTestUtils.promiseStartupManager();
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson(enterprisePolicy);
+ // Setting the enterprise policy starts the search service initialising,
+ // so we wait for that to complete before starting the test, we can
+ // then also add an extra add-on engine.
+ await Services.search.init();
+ let settingsFileWritten = promiseAfterSettings();
+ await SearchTestUtils.installSearchExtension();
+ await settingsFileWritten;
+});
+
+/**
+ * Loads the settings file and ensures it has not already been migrated.
+ */
+add_task(async function test_load_and_check_settings() {
+ let settingsTemplate = await readJSONFile(
+ do_get_file("data/search-legacy-old-loadPaths.json")
+ );
+
+ Assert.less(
+ settingsTemplate.version,
+ 8,
+ "Should be a version older than when indexing engines by id was introduced"
+ );
+ let engine = settingsTemplate.engines.find(e => e.id == "policy-Policy");
+ Assert.equal(
+ engine._loadPath,
+ "[other]addEngineWithDetails:set-via-policy",
+ "Should have a old style load path for the policy engine"
+ );
+ engine = settingsTemplate.engines.find(
+ e => e.id == "bbc163e7-7b1a-47aa-a32c-c59062de2754"
+ );
+ Assert.equal(
+ engine._loadPath,
+ "[other]addEngineWithDetails:set-via-user",
+ "Should have a old style load path for the user engine"
+ );
+ engine = settingsTemplate.engines.find(
+ e => e.id == "example@tests.mozilla.orgdefault"
+ );
+ Assert.equal(
+ engine._loadPath,
+ "[other]addEngineWithDetails:example@tests.mozilla.org",
+ "Should have a old style load path for the add-on engine"
+ );
+
+ await promiseSaveSettingsData(settingsTemplate);
+});
+
+/**
+ * Tests that an installed engine matches the expected data.
+ *
+ * @param {object} expectedData The expected data for the engine
+ */
+async function assertInstalledEngineMatches(expectedData) {
+ let engine = await Services.search.getEngineByName(expectedData.name);
+
+ Assert.ok(engine, `Should have found the ${expectedData.type} engine`);
+ Assert.equal(
+ engine.wrappedJSObject._loadPath,
+ expectedData.loadPath,
+ "Should have migrated the loadPath"
+ );
+}
+
+add_task(async function test_migration_from_pre_ids() {
+ const settingsFileWritten = promiseAfterSettings();
+
+ await Services.search.wrappedJSObject.reset();
+ await Services.search.init();
+
+ await settingsFileWritten;
+
+ await assertInstalledEngineMatches({
+ type: "Policy",
+ name: "Policy",
+ loadPath: "[policy]",
+ });
+ await assertInstalledEngineMatches({
+ type: "User",
+ name: "User",
+ loadPath: "[user]",
+ });
+ await assertInstalledEngineMatches({
+ type: "Add-on",
+ name: "Example",
+ loadPath: "[addon]example@tests.mozilla.org",
+ });
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_settings_none.js b/toolkit/components/search/tests/xpcshell/test_settings_none.js
new file mode 100644
index 0000000000..14d3bb8e51
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_settings_none.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * test_nosettings: Start search engine
+ * - without search.json.mozlz4
+ *
+ * Ensure that :
+ * - nothing explodes;
+ * - search.json.mozlz4 is created.
+ */
+
+add_task(async function setup() {
+ useHttpServer();
+ await AddonTestUtils.promiseStartupManager();
+});
+
+add_task(async function test_nosettings() {
+ let search = Services.search;
+
+ let afterSettingsPromise = promiseAfterSettings();
+
+ await search.init();
+
+ // Check that the settings is created at startup
+ await afterSettingsPromise;
+
+ // Check that search.json.mozlz4 has been created.
+ let settingsFile = do_get_profile().clone();
+ settingsFile.append(SETTINGS_FILENAME);
+ Assert.ok(settingsFile.exists());
+
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engine.xml`,
+ });
+
+ info("Engine has been added, let's wait for the settings to be built");
+ await promiseAfterSettings();
+
+ info("Searching test engine in settings");
+ let settings = await promiseSettingsData();
+ let found = false;
+ for (let engine of settings.engines) {
+ if (engine._name == "Test search engine") {
+ found = true;
+ break;
+ }
+ }
+ Assert.ok(found);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_settings_obsolete.js b/toolkit/components/search/tests/xpcshell/test_settings_obsolete.js
new file mode 100644
index 0000000000..459a9d8aa1
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_settings_obsolete.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test removing obsolete engine types on upgrade of settings.
+ */
+
+"use strict";
+
+async function loadSettingsFile(settingsFile, name) {
+ let settings = await readJSONFile(do_get_file(settingsFile));
+
+ settings.metaData.current = name;
+ settings.metaData.hash = SearchUtils.getVerificationHash(name);
+
+ await promiseSaveSettingsData(settings);
+}
+
+/**
+ * Start the search service and confirm the engine properties match the expected values.
+ *
+ * @param {string} settingsFile
+ * The path to the settings file to use.
+ * @param {string} engineName
+ * The engine name that should be default and is being removed.
+ */
+async function checkLoadSettingProperties(settingsFile, engineName) {
+ await loadSettingsFile(settingsFile, engineName);
+
+ const settingsFileWritten = promiseAfterSettings();
+ let ss = new SearchService();
+ let result = await ss.init();
+
+ Assert.ok(
+ Components.isSuccessCode(result),
+ "Should have successfully initialized the search service"
+ );
+
+ await settingsFileWritten;
+
+ let engines = await ss.getEngines();
+
+ Assert.deepEqual(
+ engines.map(e => e.name),
+ ["engine1", "engine2"],
+ "Should have only loaded the app-provided engines"
+ );
+
+ Assert.equal(
+ (await Services.search.getDefault()).name,
+ "engine1",
+ "Should have used the configured default engine"
+ );
+
+ removeSettingsFile();
+ ss._removeObservers();
+}
+
+/**
+ * Test reading from search.json.mozlz4
+ */
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("data1");
+ await AddonTestUtils.promiseStartupManager();
+});
+
+add_task(async function test_obsolete_distribution_engine() {
+ await checkLoadSettingProperties(
+ "data/search-obsolete-distribution.json",
+ "Distribution"
+ );
+});
+
+add_task(async function test_obsolete_langpack_engine() {
+ await checkLoadSettingProperties(
+ "data/search-obsolete-langpack.json",
+ "Langpack"
+ );
+});
+
+add_task(async function test_obsolete_app_engine() {
+ await checkLoadSettingProperties("data/search-obsolete-app.json", "App");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_settings_persist.js b/toolkit/components/search/tests/xpcshell/test_settings_persist.js
new file mode 100644
index 0000000000..f4ef840838
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_settings_persist.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const CONFIG_DEFAULT = [
+ {
+ webExtension: { id: "plainengine@search.mozilla.org" },
+ appliesTo: [{ included: { everywhere: true } }],
+ },
+ {
+ webExtension: { id: "special-engine@search.mozilla.org" },
+ appliesTo: [{ included: { everywhere: true } }],
+ },
+];
+
+const CONFIG_UPDATED = [
+ {
+ webExtension: { id: "plainengine@search.mozilla.org" },
+ appliesTo: [{ included: { everywhere: true } }],
+ },
+];
+
+async function startup() {
+ let settingsFileWritten = promiseAfterSettings();
+ let ss = new SearchService();
+ await AddonTestUtils.promiseRestartManager();
+ await ss.init(false);
+ await settingsFileWritten;
+ return ss;
+}
+
+async function updateConfig(config) {
+ const settings = await RemoteSettings(SearchUtils.SETTINGS_KEY);
+ settings.get.restore();
+ sinon.stub(settings, "get").returns(config);
+}
+
+async function visibleEngines(ss) {
+ return (await ss.getVisibleEngines()).map(e => e._name);
+}
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("test-extensions", null, CONFIG_DEFAULT);
+ registerCleanupFunction(AddonTestUtils.promiseShutdownManager);
+ await AddonTestUtils.promiseStartupManager();
+ // This is only needed as otherwise events will not be properly notified
+ // due to https://searchfox.org/mozilla-central/source/toolkit/components/search/SearchUtils.jsm#186
+ let settingsFileWritten = promiseAfterSettings();
+ await Services.search.init(false);
+ Services.search.wrappedJSObject._removeObservers();
+ await settingsFileWritten;
+});
+
+add_task(async function () {
+ let ss = await startup();
+ Assert.ok(
+ (await visibleEngines(ss)).includes("Special"),
+ "Should have both engines on first startup"
+ );
+
+ let settingsFileWritten = promiseAfterSettings();
+ let engine = await ss.getEngineByName("Special");
+ await ss.removeEngine(engine);
+ await settingsFileWritten;
+
+ Assert.ok(
+ !(await visibleEngines(ss)).includes("Special"),
+ "Special has been remove, only Plain should remain"
+ );
+
+ ss._removeObservers();
+ updateConfig(CONFIG_UPDATED);
+ ss = await startup();
+
+ Assert.ok(
+ !(await visibleEngines(ss)).includes("Special"),
+ "Updated to new configuration that doesnt have Special"
+ );
+
+ ss._removeObservers();
+ updateConfig(CONFIG_DEFAULT);
+ ss = await startup();
+
+ Assert.ok(
+ !(await visibleEngines(ss)).includes("Special"),
+ "Configuration now includes Special but we should remember its removal"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_settings_persist_diff_locale_same_name.js b/toolkit/components/search/tests/xpcshell/test_settings_persist_diff_locale_same_name.js
new file mode 100644
index 0000000000..3c0b930bc8
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_settings_persist_diff_locale_same_name.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const CONFIG = [
+ {
+ webExtension: { id: "engine@search.mozilla.org" },
+ appliesTo: [{ included: { everywhere: true } }],
+ },
+ {
+ // This engine has the same name, but still should be replaced correctly.
+ webExtension: {
+ id: "engine-same-name@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ excluded: { regions: ["FR"] },
+ },
+ {
+ included: { regions: ["FR"] },
+ webExtension: {
+ locales: ["gd"],
+ },
+ },
+ ],
+ },
+];
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("data", null, CONFIG);
+ await AddonTestUtils.promiseStartupManager();
+ let settingsFileWritten = promiseAfterSettings();
+ Region._setHomeRegion("US", false);
+ await Services.search.init();
+ await settingsFileWritten;
+});
+
+add_task(async function test_settings_persist_diff_locale_same_name() {
+ let settingsFileWritten = promiseAfterSettings();
+ let engine1 = Services.search.getEngineByName("engine-same-name");
+ Services.search.moveEngine(engine1, 0);
+
+ let engine2 = Services.search.getEngineByName("Test search engine");
+ Services.search.moveEngine(engine2, 1);
+ // Ensure we have saved the settings before we restart below.
+ await settingsFileWritten;
+
+ Assert.deepEqual(
+ (await Services.search.getEngines()).map(e => e.name),
+ ["engine-same-name", "Test search engine"],
+ "Should have set the engines to the expected order"
+ );
+
+ // Setting the region to FR will change the engine id, but use the same name.
+ Region._setHomeRegion("FR", false);
+
+ // Pretend we are restarting.
+ Services.search.wrappedJSObject.reset();
+ await Services.search.init();
+
+ Assert.deepEqual(
+ (await Services.search.getEngines()).map(e => e.name),
+ ["engine-same-name", "Test search engine"],
+ "Should have retained the engines in the expected order"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_sort_orders-no-hints.js b/toolkit/components/search/tests/xpcshell/test_sort_orders-no-hints.js
new file mode 100644
index 0000000000..71b75fcdc1
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sort_orders-no-hints.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check the correct default engines are picked from the configuration list,
+ * when we have some with the same orderHint, and some without any.
+ */
+
+"use strict";
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+
+ await SearchTestUtils.useTestEngines(
+ "data",
+ null,
+ (
+ await readJSONFile(do_get_file("data/engines-no-order-hint.json"))
+ ).data
+ );
+
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+});
+
+async function checkOrder(type, expectedOrder) {
+ // Reset the sorted list.
+ Services.search.wrappedJSObject._cachedSortedEngines = null;
+
+ const sortedEngines = await Services.search[type]();
+ Assert.deepEqual(
+ sortedEngines.map(s => s.name),
+ expectedOrder,
+ `Should have the expected engine order from ${type}`
+ );
+}
+
+add_task(async function test_engine_sort_with_non_builtins_sort() {
+ await SearchTestUtils.installSearchExtension({ name: "nonbuiltin1" });
+
+ // As we've added an engine, the pref will have been set to true, but
+ // we do really want to test the default sort.
+ Services.search.wrappedJSObject._settings.setMetaDataAttribute(
+ "useSavedOrder",
+ false
+ );
+
+ const EXPECTED_ORDER = [
+ // Default engine.
+ "Test search engine",
+ // Alphabetical order for the two with orderHint = 1000.
+ "engine-chromeicon",
+ "engine-rel-searchform-purpose",
+ // Alphabetical order for the remaining engines without orderHint.
+ "engine-pref",
+ "engine-resourceicon",
+ "Test search engine (Reordered)",
+ ];
+
+ // We should still have the same built-in engines listed.
+ await checkOrder("getAppProvidedEngines", EXPECTED_ORDER);
+
+ const expected = [...EXPECTED_ORDER];
+ // This is inserted in alphabetical order for the last three.
+ expected.splice(expected.length - 1, 0, "nonbuiltin1");
+ await checkOrder("getEngines", expected);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_sort_orders.js b/toolkit/components/search/tests/xpcshell/test_sort_orders.js
new file mode 100644
index 0000000000..4609c7f4fb
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sort_orders.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check the correct default engines are picked from the configuration list,
+ * and have the correct orders.
+ */
+
+"use strict";
+
+const EXPECTED_ORDER = [
+ // Default engines
+ "Test search engine",
+ "engine-pref",
+ // Now the engines in orderHint order.
+ "engine-resourceicon",
+ "engine-chromeicon",
+ "engine-rel-searchform-purpose",
+ "Test search engine (Reordered)",
+];
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+
+ await SearchTestUtils.useTestEngines();
+
+ Services.locale.availableLocales = [
+ ...Services.locale.availableLocales,
+ "gd",
+ ];
+
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ true
+ );
+});
+
+async function checkOrder(type, expectedOrder) {
+ // Reset the sorted list.
+ Services.search.wrappedJSObject._cachedSortedEngines = null;
+
+ const sortedEngines = await Services.search[type]();
+ Assert.deepEqual(
+ sortedEngines.map(s => s.name),
+ expectedOrder,
+ `Should have the expected engine order from ${type}`
+ );
+}
+
+add_task(async function test_engine_sort_only_builtins() {
+ await checkOrder("getAppProvidedEngines", EXPECTED_ORDER);
+ await checkOrder("getEngines", EXPECTED_ORDER);
+});
+
+add_task(async function test_engine_sort_with_non_builtins_sort() {
+ await SearchTestUtils.installSearchExtension({ name: "nonbuiltin1" });
+
+ // As we've added an engine, the pref will have been set to true, but
+ // we do really want to test the default sort.
+ Services.search.wrappedJSObject._settings.setMetaDataAttribute(
+ "useSavedOrder",
+ false
+ );
+
+ // We should still have the same built-in engines listed.
+ await checkOrder("getAppProvidedEngines", EXPECTED_ORDER);
+
+ const expected = [...EXPECTED_ORDER];
+ expected.splice(EXPECTED_ORDER.length, 0, "nonbuiltin1");
+ await checkOrder("getEngines", expected);
+});
+
+add_task(async function test_engine_sort_with_locale() {
+ await promiseSetLocale("gd");
+
+ const expected = [
+ "engine-resourceicon-gd",
+ "engine-pref",
+ "engine-rel-searchform-purpose",
+ "engine-chromeicon",
+ "Test search engine (Reordered)",
+ ];
+
+ await checkOrder("getAppProvidedEngines", expected);
+ expected.push("nonbuiltin1");
+ await checkOrder("getEngines", expected);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_telemetry_event_default.js b/toolkit/components/search/tests/xpcshell/test_telemetry_event_default.js
new file mode 100644
index 0000000000..a73ef6be58
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_telemetry_event_default.js
@@ -0,0 +1,293 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests for the default engine telemetry event that can be tested via xpcshell,
+ * related to changing or selecting a different configuration.
+ * Other tests are typically in browser mochitests.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+});
+
+const BASE_CONFIG = [
+ {
+ webExtension: { id: "engine@search.mozilla.org" },
+ appliesTo: [{ included: { everywhere: true } }],
+ default: "yes",
+ },
+];
+const MAIN_CONFIG = [
+ {
+ webExtension: { id: "engine@search.mozilla.org" },
+ appliesTo: [{ included: { everywhere: true } }],
+ default: "no",
+ },
+ {
+ webExtension: { id: "engine-chromeicon@search.mozilla.org" },
+ appliesTo: [{ included: { everywhere: true } }],
+ default: "yes-if-no-other",
+ },
+ {
+ webExtension: { id: "engine-fr@search.mozilla.org" },
+ appliesTo: [
+ { included: { everywhere: true } },
+ {
+ included: { locales: { matches: ["fr"] } },
+ excluded: { regions: ["DE"] },
+ default: "yes",
+ },
+ ],
+ default: "no",
+ },
+ {
+ webExtension: { id: "engine-pref@search.mozilla.org" },
+ appliesTo: [
+ { included: { everywhere: true } },
+ { included: { regions: ["DE"] }, default: "yes" },
+ ],
+ default: "no",
+ },
+ {
+ webExtension: { id: "engine2@search.mozilla.org" },
+ appliesTo: [
+ { included: { everywhere: true } },
+ { included: { everywhere: true }, experiment: "test1", default: "yes" },
+ ],
+ default: "no",
+ },
+];
+
+const testSearchEngine = {
+ id: "engine",
+ name: "Test search engine",
+ loadPath: "[addon]engine@search.mozilla.org",
+ submissionUrl: "https://www.google.com/search?q=",
+};
+const testChromeIconEngine = {
+ id: "engine-chromeicon",
+ name: "engine-chromeicon",
+ loadPath: "[addon]engine-chromeicon@search.mozilla.org",
+ submissionUrl: "https://www.google.com/search?q=",
+};
+const testFrEngine = {
+ id: "engine-fr",
+ name: "Test search engine (fr)",
+ loadPath: "[addon]engine-fr@search.mozilla.org",
+ submissionUrl: "https://www.google.fr/search?q=&ie=iso-8859-1&oe=iso-8859-1",
+};
+const testPrefEngine = {
+ id: "engine-pref",
+ name: "engine-pref",
+ loadPath: "[addon]engine-pref@search.mozilla.org",
+ submissionUrl: "https://www.google.com/search?q=",
+};
+const testEngine2 = {
+ id: "engine2",
+ name: "A second test engine",
+ loadPath: "[addon]engine2@search.mozilla.org",
+ submissionUrl: "https://duckduckgo.com/?q=",
+};
+
+function clearTelemetry() {
+ Services.telemetry.clearEvents();
+ Services.fog.testResetFOG();
+}
+
+async function checkTelemetry(
+ source,
+ prevEngine,
+ newEngine,
+ checkPrivate = false
+) {
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: checkPrivate ? "change_private" : "change_default",
+ value: source,
+ extra: {
+ prev_id: prevEngine?.id ?? "",
+ new_id: newEngine?.id ?? "",
+ new_name: newEngine?.name ?? "",
+ new_load_path: newEngine?.loadPath ?? "",
+ // Telemetry has a limit of 80 characters.
+ new_sub_url: newEngine?.submissionUrl.slice(0, 80) ?? "",
+ },
+ },
+ ],
+ { category: "search", method: "engine" }
+ );
+
+ let snapshot;
+ if (checkPrivate) {
+ snapshot = await Glean.searchEnginePrivate.changed.testGetValue();
+ } else {
+ snapshot = await Glean.searchEngineDefault.changed.testGetValue();
+ }
+ delete snapshot[0].timestamp;
+ Assert.deepEqual(
+ snapshot[0],
+ {
+ category: checkPrivate
+ ? "search.engine.private"
+ : "search.engine.default",
+ name: "changed",
+ extra: {
+ change_source: source,
+ previous_engine_id: prevEngine?.id ?? "",
+ new_engine_id: newEngine?.id ?? "",
+ new_display_name: newEngine?.name ?? "",
+ new_load_path: newEngine?.loadPath ?? "",
+ new_submission_url: newEngine?.submissionUrl ?? "",
+ },
+ },
+ "Should have received the correct event details"
+ );
+}
+
+let getVariableStub;
+
+add_setup(async () => {
+ Region._setHomeRegion("US", false);
+ Services.locale.availableLocales = [
+ ...Services.locale.availableLocales,
+ "en",
+ "fr",
+ ];
+ Services.locale.requestedLocales = ["en"];
+
+ sinon.spy(NimbusFeatures.searchConfiguration, "onUpdate");
+ sinon.stub(NimbusFeatures.searchConfiguration, "ready").resolves();
+ getVariableStub = sinon.stub(
+ NimbusFeatures.searchConfiguration,
+ "getVariable"
+ );
+ getVariableStub.returns(null);
+
+ SearchTestUtils.useMockIdleService();
+ Services.fog.initializeFOG();
+ sinon.stub(
+ Services.search.wrappedJSObject,
+ "_showRemovalOfSearchEngineNotificationBox"
+ );
+
+ await SearchTestUtils.useTestEngines("data", null, BASE_CONFIG);
+ await AddonTestUtils.promiseStartupManager();
+
+ await Services.search.init();
+});
+
+add_task(async function test_configuration_changes_default() {
+ clearTelemetry();
+
+ await SearchTestUtils.updateRemoteSettingsConfig(MAIN_CONFIG);
+
+ await checkTelemetry("config", testSearchEngine, testChromeIconEngine);
+});
+
+add_task(async function test_experiment_changes_default() {
+ clearTelemetry();
+
+ let reloadObserved =
+ SearchTestUtils.promiseSearchNotification("engines-reloaded");
+ getVariableStub.callsFake(name => (name == "experiment" ? "test1" : null));
+ NimbusFeatures.searchConfiguration.onUpdate.firstCall.args[0]();
+ await reloadObserved;
+
+ await checkTelemetry("experiment", testChromeIconEngine, testEngine2);
+
+ // Reset the stub so that we are no longer in an experiment.
+ getVariableStub.returns(null);
+});
+
+add_task(async function test_locale_changes_default() {
+ clearTelemetry();
+
+ let reloadObserved =
+ SearchTestUtils.promiseSearchNotification("engines-reloaded");
+ Services.locale.requestedLocales = ["fr"];
+ await reloadObserved;
+
+ await checkTelemetry("locale", testEngine2, testFrEngine);
+});
+
+add_task(async function test_region_changes_default() {
+ clearTelemetry();
+
+ let reloadObserved =
+ SearchTestUtils.promiseSearchNotification("engines-reloaded");
+ Region._setHomeRegion("DE", true);
+ await reloadObserved;
+
+ await checkTelemetry("region", testFrEngine, testPrefEngine);
+});
+
+add_task(async function test_user_changes_separate_private_pref() {
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ true
+ );
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+
+ await Services.search.setDefaultPrivate(
+ Services.search.getEngineByName("engine-chromeicon"),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ Assert.notEqual(
+ await Services.search.getDefault(),
+ await Services.search.getDefaultPrivate(),
+ "Should have different engines for the pre-condition"
+ );
+
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ false
+ );
+
+ clearTelemetry();
+
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ false
+ );
+
+ await checkTelemetry("user_private_split", testChromeIconEngine, null, true);
+
+ getVariableStub.returns(null);
+});
+
+add_task(async function test_experiment_with_separate_default_notifies() {
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ false
+ );
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+
+ clearTelemetry();
+
+ getVariableStub.callsFake(name =>
+ name == "seperatePrivateDefaultUIEnabled" ? true : null
+ );
+ NimbusFeatures.searchConfiguration.onUpdate.firstCall.args[0]();
+
+ await checkTelemetry("experiment", null, testChromeIconEngine, true);
+
+ clearTelemetry();
+
+ // Reset the stub so that we are no longer in an experiment.
+ getVariableStub.returns(null);
+ NimbusFeatures.searchConfiguration.onUpdate.firstCall.args[0]();
+
+ await checkTelemetry("experiment", testChromeIconEngine, null, true);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_userEngine.js b/toolkit/components/search/tests/xpcshell/test_userEngine.js
new file mode 100644
index 0000000000..659baf10ab
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_userEngine.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that User Engines can be installed correctly.
+ */
+
+"use strict";
+
+add_task(async function setup() {
+ Services.fog.initializeFOG();
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+});
+
+add_task(async function test_user_engine() {
+ let promiseEngineAdded = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.ADDED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ await Services.search.addUserEngine(
+ "user",
+ "https://example.com/user?q={searchTerms}",
+ "u"
+ );
+ await promiseEngineAdded;
+
+ let engine = Services.search.getEngineByName("user");
+ Assert.ok(engine, "Should have installed the engine.");
+
+ Assert.equal(engine.name, "user", "Should have the correct name");
+ Assert.equal(engine.description, null, "Should not have a description");
+ Assert.deepEqual(engine.aliases, ["u"], "Should have the correct alias");
+
+ let submission = engine.getSubmission("foo");
+ Assert.equal(
+ submission.uri.spec,
+ "https://example.com/user?q=foo",
+ "Should have the correct search url"
+ );
+
+ submission = engine.getSubmission("foo", SearchUtils.URL_TYPE.SUGGEST_JSON);
+ Assert.equal(submission, null, "Should not have a suggest url");
+
+ Services.search.defaultEngine = engine;
+
+ await assertGleanDefaultEngine({
+ normal: {
+ engineId: "other-user",
+ displayName: "user",
+ loadPath: "[user]",
+ submissionUrl: "blank:",
+ verified: "verified",
+ },
+ });
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_validate_engines.js b/toolkit/components/search/tests/xpcshell/test_validate_engines.js
new file mode 100644
index 0000000000..1f95b1e14b
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_validate_engines.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Ensure all the engines defined in the configuration are valid by
+// creating a refined configuration that includes all the engines everywhere.
+
+"use strict";
+
+const { SearchService } = ChromeUtils.importESModule(
+ "resource://gre/modules/SearchService.sys.mjs"
+);
+
+const ss = new SearchService();
+
+add_task(async function test_validate_engines() {
+ let settings = RemoteSettings(SearchUtils.SETTINGS_KEY);
+ let config = await settings.get();
+ config = config.map(e => {
+ return {
+ appliesTo: [
+ {
+ included: {
+ everywhere: true,
+ },
+ },
+ ],
+ webExtension: {
+ id: e.webExtension.id,
+ },
+ };
+ });
+
+ sinon.stub(settings, "get").returns(config);
+ await AddonTestUtils.promiseStartupManager();
+ await ss.init();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_validate_manifests.js b/toolkit/components/search/tests/xpcshell/test_validate_manifests.js
new file mode 100644
index 0000000000..c840009ab6
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_validate_manifests.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.importGlobalProperties(["fetch"]);
+
+const { ExtensionData } = ChromeUtils.importESModule(
+ "resource://gre/modules/Extension.sys.mjs"
+);
+
+const SEARCH_EXTENSIONS_PATH = "resource://search-extensions";
+
+function getFileURI(resourceURI) {
+ let resHandler = Services.io
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ let filePath = resHandler.resolveURI(Services.io.newURI(resourceURI));
+ return Services.io.newURI(filePath);
+}
+
+async function getSearchExtensions() {
+ // Fetching the root will give us the directory listing which we can parse
+ // for each file name
+ let list = await fetch(`${SEARCH_EXTENSIONS_PATH}/`).then(req => req.text());
+ return list
+ .split("\n")
+ .slice(2)
+ .reduce((acc, line) => {
+ let parts = line.split(" ");
+ if (parts.length > 2 && !parts[1].endsWith(".json")) {
+ // When the directory listing comes from omni jar each engine
+ // has a trailing slash (engine/) which we dont get locally, or want.
+ acc.push(parts[1].split("/")[0]);
+ }
+ return acc;
+ }, []);
+}
+
+add_task(async function test_validate_manifest() {
+ let searchExtensions = await getSearchExtensions();
+ ok(
+ !!searchExtensions.length,
+ `Found ${searchExtensions.length} search extensions`
+ );
+ for (const xpi of searchExtensions) {
+ info(`loading: ${SEARCH_EXTENSIONS_PATH}/${xpi}/`);
+ let fileURI = getFileURI(`${SEARCH_EXTENSIONS_PATH}/${xpi}/`);
+ let extension = new ExtensionData(fileURI, false);
+ await extension.loadManifest();
+ let locales = await extension.promiseLocales();
+ for (let locale of locales.keys()) {
+ try {
+ let manifest = await extension.getLocalizedManifest(locale);
+ ok(!!manifest, `parsed manifest ${xpi.leafName} in ${locale}`);
+ } catch (e) {
+ ok(
+ false,
+ `FAIL manifest for ${xpi.leafName} in locale ${locale} failed ${e} :: ${e.stack}`
+ );
+ }
+ }
+ }
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_webextensions_builtin_upgrade.js b/toolkit/components/search/tests/xpcshell/test_webextensions_builtin_upgrade.js
new file mode 100644
index 0000000000..41a7c0c0b2
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_webextensions_builtin_upgrade.js
@@ -0,0 +1,252 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Enable SCOPE_APPLICATION for builtin testing. Default in tests is only SCOPE_PROFILE.
+// AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION == 5;
+Services.prefs.setIntPref("extensions.enabledScopes", 5);
+
+const { promiseShutdownManager, promiseStartupManager } = AddonTestUtils;
+
+const TEST_CONFIG = [
+ {
+ webExtension: {
+ id: "multilocale@search.mozilla.org",
+ locales: ["af", "an"],
+ },
+ appliesTo: [{ included: { everywhere: true } }],
+ },
+ {
+ webExtension: {
+ id: "plainengine@search.mozilla.org",
+ },
+ appliesTo: [{ included: { everywhere: true } }],
+ params: {
+ searchUrlGetParams: [
+ {
+ name: "config",
+ value: "applied",
+ },
+ ],
+ },
+ },
+];
+
+async function getEngineNames() {
+ let engines = await Services.search.getEngines();
+ return engines.map(engine => engine._name);
+}
+
+function makePlainExtension(version, name = "Plain") {
+ return {
+ useAddonManager: "permanent",
+ manifest: {
+ name,
+ version,
+ browser_specific_settings: {
+ gecko: {
+ id: "plainengine@search.mozilla.org",
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name,
+ search_url: "https://duckduckgo.com/",
+ params: [
+ {
+ name: "q",
+ value: "{searchTerms}",
+ },
+ {
+ name: "t",
+ condition: "purpose",
+ purpose: "contextmenu",
+ value: "ffcm",
+ },
+ {
+ name: "t",
+ condition: "purpose",
+ purpose: "keyword",
+ value: "ffab",
+ },
+ {
+ name: "t",
+ condition: "purpose",
+ purpose: "searchbar",
+ value: "ffsb",
+ },
+ {
+ name: "t",
+ condition: "purpose",
+ purpose: "homepage",
+ value: "ffhp",
+ },
+ {
+ name: "t",
+ condition: "purpose",
+ purpose: "newtab",
+ value: "ffnt",
+ },
+ ],
+ suggest_url: "https://ac.duckduckgo.com/ac/q={searchTerms}&type=list",
+ },
+ },
+ },
+ };
+}
+
+function makeMultiLocaleExtension(version) {
+ return {
+ useAddonManager: "permanent",
+ manifest: {
+ name: "__MSG_searchName__",
+ version,
+ browser_specific_settings: {
+ gecko: {
+ id: "multilocale@search.mozilla.org",
+ },
+ },
+ default_locale: "an",
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "__MSG_searchName__",
+ search_url: "__MSG_searchUrl__",
+ },
+ },
+ },
+ files: {
+ "_locales/af/messages.json": {
+ searchUrl: {
+ message: `https://example.af/?q={searchTerms}&version=${version}`,
+ description: "foo",
+ },
+ searchName: {
+ message: `Multilocale AF`,
+ description: "foo",
+ },
+ },
+ "_locales/an/messages.json": {
+ searchUrl: {
+ message: `https://example.an/?q={searchTerms}&version=${version}`,
+ description: "foo",
+ },
+ searchName: {
+ message: `Multilocale AN`,
+ description: "foo",
+ },
+ },
+ },
+ };
+}
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("test-extensions", null, TEST_CONFIG);
+ await promiseStartupManager();
+
+ registerCleanupFunction(promiseShutdownManager);
+ await Services.search.init();
+});
+
+add_task(async function basic_multilocale_test() {
+ Assert.deepEqual(await getEngineNames(), [
+ "Multilocale AF",
+ "Multilocale AN",
+ "Plain",
+ ]);
+
+ let ext = ExtensionTestUtils.loadExtension(makeMultiLocaleExtension("2.0"));
+ await ext.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext);
+
+ Assert.deepEqual(await getEngineNames(), [
+ "Multilocale AF",
+ "Multilocale AN",
+ "Plain",
+ ]);
+
+ let engine = await Services.search.getEngineByName("Multilocale AF");
+ Assert.equal(
+ engine.getSubmission("test").uri.spec,
+ "https://example.af/?q=test&version=2.0",
+ "Engine got update"
+ );
+ engine = await Services.search.getEngineByName("Multilocale AN");
+ Assert.equal(
+ engine.getSubmission("test").uri.spec,
+ "https://example.an/?q=test&version=2.0",
+ "Engine got update"
+ );
+
+ await ext.unload();
+});
+
+add_task(async function upgrade_with_configuration_change_test() {
+ Assert.deepEqual(await getEngineNames(), [
+ "Multilocale AF",
+ "Multilocale AN",
+ "Plain",
+ ]);
+
+ let engine = await Services.search.getEngineByName("Plain");
+ Assert.ok(engine.isAppProvided);
+ Assert.equal(
+ engine.getSubmission("test").uri.spec,
+ // This test engine specifies the q and t params in its search_url, therefore
+ // we get both those and the extra parameter specified in the test config.
+ "https://duckduckgo.com/?q=test&t=ffsb&config=applied",
+ "Should have the configuration applied before update."
+ );
+
+ let ext = ExtensionTestUtils.loadExtension(makePlainExtension("2.0"));
+ await ext.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext);
+
+ Assert.deepEqual(await getEngineNames(), [
+ "Multilocale AF",
+ "Multilocale AN",
+ "Plain",
+ ]);
+
+ engine = await Services.search.getEngineByName("Plain");
+ Assert.equal(
+ engine.getSubmission("test").uri.spec,
+ // This test engine specifies the q and t params in its search_url, therefore
+ // we get both those and the extra parameter specified in the test config.
+ "https://duckduckgo.com/?q=test&t=ffsb&config=applied",
+ "Should still have the configuration applied after update."
+ );
+
+ await ext.unload();
+});
+
+add_task(async function test_upgrade_with_name_change() {
+ Assert.deepEqual(await getEngineNames(), [
+ "Multilocale AF",
+ "Multilocale AN",
+ "Plain",
+ ]);
+
+ let ext = ExtensionTestUtils.loadExtension(
+ makePlainExtension("2.0", "Plain2")
+ );
+ await ext.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext);
+
+ Assert.deepEqual(await getEngineNames(), [
+ "Multilocale AF",
+ "Multilocale AN",
+ "Plain2",
+ ]);
+
+ let engine = await Services.search.getEngineByName("Plain2");
+ Assert.equal(
+ engine.getSubmission("test").uri.spec,
+ // This test engine specifies the q and t params in its search_url, therefore
+ // we get both those and the extra parameter specified in the test config.
+ "https://duckduckgo.com/?q=test&t=ffsb&config=applied",
+ "Should still have the configuration applied after update."
+ );
+
+ await ext.unload();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_webextensions_install.js b/toolkit/components/search/tests/xpcshell/test_webextensions_install.js
new file mode 100644
index 0000000000..4b006edd6c
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_webextensions_install.js
@@ -0,0 +1,205 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { promiseShutdownManager, promiseStartupManager } = AddonTestUtils;
+
+async function getEngineNames() {
+ let engines = await Services.search.getEngines();
+ return engines.map(engine => engine._name);
+}
+
+add_task(async function setup() {
+ let server = useHttpServer();
+ server.registerContentType("sjs", "sjs");
+ await SearchTestUtils.useTestEngines("test-extensions");
+ await promiseStartupManager();
+
+ Services.locale.availableLocales = [
+ ...Services.locale.availableLocales,
+ "af",
+ ];
+
+ registerCleanupFunction(async () => {
+ await promiseShutdownManager();
+ Services.prefs.clearUserPref("browser.search.region");
+ });
+});
+
+add_task(async function basic_install_test() {
+ await Services.search.init();
+ await promiseAfterSettings();
+
+ // On first boot, we get the configuration defaults
+ Assert.deepEqual(await getEngineNames(), ["Plain", "Special"]);
+
+ // User installs a new search engine
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ encoding: "windows-1252",
+ },
+ { skipUnload: true }
+ );
+ Assert.deepEqual((await getEngineNames()).sort(), [
+ "Example",
+ "Plain",
+ "Special",
+ ]);
+
+ let engine = await Services.search.getEngineByName("Example");
+ Assert.equal(
+ engine.wrappedJSObject.queryCharset,
+ "windows-1252",
+ "Should have the correct charset"
+ );
+
+ // User uninstalls their engine
+ await extension.awaitStartup();
+ await extension.unload();
+ await promiseAfterSettings();
+ Assert.deepEqual(await getEngineNames(), ["Plain", "Special"]);
+});
+
+add_task(async function test_install_duplicate_engine() {
+ let name = "Plain";
+ consoleAllowList.push(`An engine called ${name} already exists`);
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name,
+ search_url: "https://example.com/plain",
+ },
+ { skipUnload: true }
+ );
+
+ let engine = await Services.search.getEngineByName("Plain");
+ let submission = engine.getSubmission("foo");
+ Assert.equal(
+ submission.uri.spec,
+ "https://duckduckgo.com/?q=foo&t=ffsb",
+ "Should have not changed the app provided engine."
+ );
+
+ // User uninstalls their engine
+ await extension.unload();
+});
+
+add_task(async function basic_multilocale_test() {
+ await promiseSetHomeRegion("an");
+
+ Assert.deepEqual(await getEngineNames(), [
+ "Plain",
+ "Special",
+ "Multilocale AN",
+ ]);
+});
+
+add_task(async function complex_multilocale_test() {
+ await promiseSetHomeRegion("af");
+
+ Assert.deepEqual(await getEngineNames(), [
+ "Plain",
+ "Special",
+ "Multilocale AF",
+ "Multilocale AN",
+ ]);
+});
+
+add_task(async function test_manifest_selection() {
+ // Sets the home region without updating.
+ Region._setHomeRegion("an", false);
+ await promiseSetLocale("af");
+
+ let engine = await Services.search.getEngineByName("Multilocale AN");
+ Assert.ok(
+ engine.iconURI.spec.endsWith("favicon-an.ico"),
+ "Should have the correct favicon for an extension of one locale using a different locale."
+ );
+ Assert.equal(
+ engine.description,
+ "A enciclopedia Libre",
+ "Should have the correct engine name for an extension of one locale using a different locale."
+ );
+});
+
+add_task(async function test_load_favicon_invalid() {
+ let observed = TestUtils.consoleMessageObserved(msg => {
+ return msg.wrappedJSObject.arguments[0].includes(
+ "Content type does not match expected"
+ );
+ });
+
+ // User installs a new search engine
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ favicon_url: `${gDataUrl}engine.xml`,
+ },
+ { skipUnload: true }
+ );
+
+ await observed;
+
+ let engine = await Services.search.getEngineByName("Example");
+ Assert.equal(null, engine.iconURI, "Should not have set an iconURI");
+
+ // User uninstalls their engine
+ await extension.awaitStartup();
+ await extension.unload();
+ await promiseAfterSettings();
+});
+
+add_task(async function test_load_favicon_invalid_redirect() {
+ let observed = TestUtils.consoleMessageObserved(msg => {
+ return msg.wrappedJSObject.arguments[0].includes(
+ "Content type does not match expected"
+ );
+ });
+
+ // User installs a new search engine
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ favicon_url: `${gDataUrl}/iconsRedirect.sjs?type=invalid`,
+ },
+ { skipUnload: true }
+ );
+
+ await observed;
+
+ let engine = await Services.search.getEngineByName("Example");
+ Assert.equal(null, engine.iconURI, "Should not have set an iconURI");
+
+ // User uninstalls their engine
+ await extension.awaitStartup();
+ await extension.unload();
+ await promiseAfterSettings();
+});
+
+add_task(async function test_load_favicon_redirect() {
+ let promiseEngineChanged = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+
+ // User installs a new search engine
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ favicon_url: `${gDataUrl}/iconsRedirect.sjs`,
+ },
+ { skipUnload: true }
+ );
+
+ let engine = await Services.search.getEngineByName("Example");
+
+ await promiseEngineChanged;
+
+ Assert.ok(engine.iconURI, "Should have set an iconURI");
+ Assert.ok(
+ engine.iconURI.spec.startsWith("data:image/x-icon;base64,"),
+ "Should have saved the expected content type for the icon"
+ );
+
+ // User uninstalls their engine
+ await extension.awaitStartup();
+ await extension.unload();
+ await promiseAfterSettings();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_webextensions_language_switch.js b/toolkit/components/search/tests/xpcshell/test_webextensions_language_switch.js
new file mode 100644
index 0000000000..e23a2ff433
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_webextensions_language_switch.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { promiseShutdownManager, promiseStartupManager } = AddonTestUtils;
+
+add_task(async function setup() {
+ Services.locale.availableLocales = [
+ ...Services.locale.availableLocales,
+ "en",
+ "de",
+ "fr",
+ ];
+ Services.locale.requestedLocales = ["en"];
+
+ await SearchTestUtils.useTestEngines("data1");
+ await promiseStartupManager();
+ await Services.search.init();
+ await promiseAfterSettings();
+
+ registerCleanupFunction(promiseShutdownManager);
+});
+
+add_task(async function test_language_switch_changes_name() {
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "__MSG_engineName__",
+ id: "engine@tests.mozilla.org",
+ search_url_get_params: `q={searchTerms}&version=1.0`,
+ default_locale: "en",
+ version: "1.0",
+ },
+ { skipUnload: false },
+ {
+ "_locales/en/messages.json": {
+ engineName: {
+ message: "English Name",
+ description: "The Name",
+ },
+ },
+ "_locales/fr/messages.json": {
+ engineName: {
+ message: "French Name",
+ description: "The Name",
+ },
+ },
+ }
+ );
+
+ let engine = Services.search.getEngineById("engine@tests.mozilla.orgdefault");
+ Assert.ok(!!engine, "Should have loaded the engine");
+ Assert.equal(
+ engine.name,
+ "English Name",
+ "Should have loaded the English version of the name"
+ );
+
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ let promiseChanged = TestUtils.topicObserved(
+ "browser-search-engine-modified",
+ (eng, verb) => verb == "engine-changed"
+ );
+
+ await promiseSetLocale("fr");
+
+ await promiseChanged;
+
+ engine = Services.search.getEngineById("engine@tests.mozilla.orgdefault");
+ Assert.ok(!!engine, "Should still be available");
+ Assert.equal(
+ engine.name,
+ "French Name",
+ "Should have updated to the French version of the name"
+ );
+
+ Assert.equal(
+ (await Services.search.getDefault()).id,
+ engine.id,
+ "Should have kept the default engine the same"
+ );
+
+ promiseChanged = TestUtils.topicObserved(
+ "browser-search-engine-modified",
+ (eng, verb) => verb == "engine-changed"
+ );
+
+ // Check for changing to a locale the add-on doesn't have.
+ await promiseSetLocale("de");
+
+ await promiseChanged;
+
+ engine = Services.search.getEngineById("engine@tests.mozilla.orgdefault");
+ Assert.ok(!!engine, "Should still be available");
+ Assert.equal(
+ engine.name,
+ "English Name",
+ "Should have fallen back to the default locale (English) version of the name"
+ );
+
+ Assert.equal(
+ (await Services.search.getDefault()).id,
+ engine.id,
+ "Should have kept the default engine the same"
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_webextensions_migrate_to.js b/toolkit/components/search/tests/xpcshell/test_webextensions_migrate_to.js
new file mode 100644
index 0000000000..b9e78203ca
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_webextensions_migrate_to.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test migrating legacy add-on engines in background.
+ */
+
+"use strict";
+
+add_task(async function setup() {
+ useHttpServer("opensearch");
+ await AddonTestUtils.promiseStartupManager();
+ await SearchTestUtils.useTestEngines("data1");
+
+ let data = await readJSONFile(do_get_file("data/search-migration.json"));
+
+ await promiseSaveSettingsData(data);
+
+ await Services.search.init();
+
+ // We need the extension installed for this test, but we do not want to
+ // trigger the functions that happen on installation, so stub that out.
+ // The manifest already has details of this engine.
+ let oldFunc = Services.search.wrappedJSObject.addEnginesFromExtension;
+ Services.search.wrappedJSObject.addEnginesFromExtension = () => {};
+
+ // Add the add-on so add-on manager has a valid item.
+ await SearchTestUtils.installSearchExtension({
+ id: "simple",
+ name: "simple search",
+ search_url: "https://example.com/",
+ });
+
+ Services.search.wrappedJSObject.addEnginesFromExtension = oldFunc;
+});
+
+add_task(async function test_migrateLegacyEngineDifferentName() {
+ await Services.search.init();
+
+ let engine = Services.search.getEngineByName("simple");
+ Assert.ok(engine, "Should have the legacy add-on engine.");
+
+ // Set this engine as default, the new engine should become the default
+ // after migration.
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ engine = Services.search.getEngineByName("simple search");
+ Assert.ok(engine, "Should have the WebExtension engine.");
+
+ await Services.search.runBackgroundChecks();
+
+ engine = Services.search.getEngineByName("simple");
+ Assert.ok(!engine, "Should have removed the legacy add-on engine");
+
+ engine = Services.search.getEngineByName("simple search");
+ Assert.ok(engine, "Should have kept the WebExtension engine.");
+
+ Assert.equal(
+ (await Services.search.getDefault()).name,
+ engine.name,
+ "Should have switched to the WebExtension engine as default."
+ );
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_webextensions_normandy_upgrade.js b/toolkit/components/search/tests/xpcshell/test_webextensions_normandy_upgrade.js
new file mode 100644
index 0000000000..faf57c1807
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_webextensions_normandy_upgrade.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+SearchTestUtils.initXPCShellAddonManager(this, "system");
+
+async function restart() {
+ Services.search.wrappedJSObject.reset();
+ await AddonTestUtils.promiseRestartManager();
+ await Services.search.init(false);
+}
+
+const CONFIG_DEFAULT = [
+ {
+ webExtension: { id: "plainengine@search.mozilla.org" },
+ appliesTo: [{ included: { everywhere: true } }],
+ },
+];
+
+const CONFIG_UPDATED = [
+ {
+ webExtension: { id: "plainengine@search.mozilla.org" },
+ appliesTo: [{ included: { everywhere: true } }],
+ },
+ {
+ webExtension: { id: "example@search.mozilla.org" },
+ appliesTo: [{ included: { everywhere: true } }],
+ },
+];
+
+async function getEngineNames() {
+ let engines = await Services.search.getAppProvidedEngines();
+ return engines.map(engine => engine._name);
+}
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("test-extensions", null, CONFIG_DEFAULT);
+ await AddonTestUtils.promiseStartupManager();
+ registerCleanupFunction(AddonTestUtils.promiseShutdownManager);
+ SearchTestUtils.useMockIdleService();
+ await Services.search.init();
+});
+
+// Test the situation where we receive an updated configuration
+// that references an engine that doesnt exist locally as it
+// will be installed by Normandy.
+add_task(async function test_config_before_normandy() {
+ // Ensure initial default setup.
+ await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_DEFAULT);
+ await restart();
+ Assert.deepEqual(await getEngineNames(), ["Plain"]);
+ // Updated configuration references nonexistant engine.
+ await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_UPDATED);
+ Assert.deepEqual(
+ await getEngineNames(),
+ ["Plain"],
+ "Updated engine hasnt been installed yet"
+ );
+ // Normandy then installs the engine.
+ let addon = await SearchTestUtils.installSystemSearchExtension();
+ Assert.deepEqual(
+ await getEngineNames(),
+ ["Plain", "Example"],
+ "Both engines are now enabled"
+ );
+ await addon.unload();
+});
+
+// Test the situation where we receive a newly installed
+// engine from Normandy followed by the update to the
+// configuration that uses that engine.
+add_task(async function test_normandy_before_config() {
+ // Ensure initial default setup.
+ await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_DEFAULT);
+ await restart();
+ Assert.deepEqual(await getEngineNames(), ["Plain"]);
+ // Normandy installs the enigne.
+ let addon = await SearchTestUtils.installSystemSearchExtension();
+ Assert.deepEqual(
+ await getEngineNames(),
+ ["Plain"],
+ "Normandy engine ignored as not in config yet"
+ );
+ // Configuration is updated to use the engine.
+ await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_UPDATED);
+ Assert.deepEqual(
+ await getEngineNames(),
+ ["Plain", "Example"],
+ "Both engines are now enabled"
+ );
+ await addon.unload();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_webextensions_startup_remove.js b/toolkit/components/search/tests/xpcshell/test_webextensions_startup_remove.js
new file mode 100644
index 0000000000..0b55bca424
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_webextensions_startup_remove.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const ENGINE_ID = "enginetest@example.com";
+let xpi;
+let profile = do_get_profile().clone();
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("data1");
+ xpi = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: { id: ENGINE_ID },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "Test Engine",
+ search_url: `https://example.com/?q={searchTerms}`,
+ },
+ },
+ },
+ });
+ await AddonTestUtils.manuallyInstall(xpi);
+});
+
+add_task(async function test_removeAddonOnStartup() {
+ // First startup the add-on manager and ensure the engine is installed.
+ await AddonTestUtils.promiseStartupManager();
+ let promise = promiseAfterSettings();
+ await Services.search.init();
+
+ let engine = Services.search.getEngineByName("Test Engine");
+ let allEngines = await Services.search.getEngines();
+
+ Assert.ok(!!engine, "Should have installed the test engine");
+
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await promise;
+
+ await AddonTestUtils.promiseShutdownManager();
+
+ // Now remove it, reset the search service and start up the add-on manager.
+ // Note: the saved settings will have the engine in. If this didn't work,
+ // the engine would still be present.
+ await IOUtils.remove(
+ PathUtils.join(profile.path, "extensions", `${ENGINE_ID}.xpi`)
+ );
+
+ let removePromise = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.REMOVED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ Services.search.wrappedJSObject.reset();
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+ await removePromise;
+
+ Assert.ok(
+ !Services.search.getEngineByName("Test Engine"),
+ "Should have removed the test engine"
+ );
+
+ let newEngines = await Services.search.getEngines();
+ Assert.deepEqual(
+ newEngines.map(e => e.name),
+ allEngines.map(e => e.name).filter(n => n != "Test Engine"),
+ "Should no longer have the test engine in the full list"
+ );
+ let newDefault = await Services.search.getDefault();
+ Assert.equal(
+ newDefault.name,
+ "engine1",
+ "Should have changed the default engine back to the configuration default"
+ );
+
+ await AddonTestUtils.promiseShutdownManager();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_webextensions_upgrade.js b/toolkit/components/search/tests/xpcshell/test_webextensions_upgrade.js
new file mode 100644
index 0000000000..555e2ae2f1
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_webextensions_upgrade.js
@@ -0,0 +1,188 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { promiseShutdownManager, promiseStartupManager } = AddonTestUtils;
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("data1");
+ await promiseStartupManager();
+ await Services.search.init();
+ await promiseAfterSettings();
+
+ registerCleanupFunction(promiseShutdownManager);
+});
+
+add_task(async function test_basic_upgrade() {
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ version: "1.0",
+ search_url_get_params: `q={searchTerms}&version=1.0`,
+ keyword: "foo",
+ },
+ { skipUnload: true }
+ );
+
+ let engine = await Services.search.getEngineByAlias("foo");
+ Assert.ok(engine, "Can fetch engine with alias");
+ engine.alias = "testing";
+
+ engine = await Services.search.getEngineByAlias("testing");
+ Assert.ok(engine, "Can fetch engine by alias");
+ let params = engine.getSubmission("test").uri.query.split("&");
+ Assert.ok(params.includes("version=1.0"), "Correct version installed");
+
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ let promiseChanged = TestUtils.topicObserved(
+ "browser-search-engine-modified",
+ (eng, verb) => verb == "engine-changed"
+ );
+
+ let manifest = SearchTestUtils.createEngineManifest({
+ version: "2.0",
+ search_url_get_params: `q={searchTerms}&version=2.0`,
+ keyword: "bar",
+ });
+ await extension.upgrade({
+ useAddonManager: "permanent",
+ manifest,
+ });
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+ await promiseChanged;
+
+ engine = await Services.search.getEngineByAlias("testing");
+ Assert.ok(engine, "Engine still has alias set");
+
+ params = engine.getSubmission("test").uri.query.split("&");
+ Assert.ok(params.includes("version=2.0"), "Correct version installed");
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "Example",
+ "Should have retained the same default engine"
+ );
+
+ await extension.unload();
+ await promiseAfterSettings();
+});
+
+add_task(async function test_upgrade_changes_name() {
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "engine",
+ id: "engine@tests.mozilla.org",
+ search_url_get_params: `q={searchTerms}&version=1.0`,
+ version: "1.0",
+ },
+ { skipUnload: true }
+ );
+
+ let engine = Services.search.getEngineByName("engine");
+ Assert.ok(!!engine, "Should have loaded the engine");
+
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ // When we add engines currently, we normally force using the saved order.
+ // Reset that here, so we can check the order is reset in the case this
+ // is a application provided engine change.
+ Services.search.wrappedJSObject._settings.setMetaDataAttribute(
+ "useSavedOrder",
+ false
+ );
+ Services.search.getEngineByName("engine1").wrappedJSObject._orderHint = null;
+ Services.search.getEngineByName("engine2").wrappedJSObject._orderHint = null;
+
+ Assert.deepEqual(
+ (await Services.search.getVisibleEngines()).map(e => e.name),
+ ["engine1", "engine2", "engine"],
+ "Should have the expected order initially"
+ );
+
+ let promiseChanged = TestUtils.topicObserved(
+ "browser-search-engine-modified",
+ (eng, verb) => verb == "engine-changed"
+ );
+
+ let manifest = SearchTestUtils.createEngineManifest({
+ name: "Bar",
+ id: "engine@tests.mozilla.org",
+ search_url_get_params: `q={searchTerms}&version=2.0`,
+ version: "2.0",
+ });
+ await extension.upgrade({
+ useAddonManager: "permanent",
+ manifest,
+ });
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+
+ await promiseChanged;
+
+ engine = Services.search.getEngineByName("Bar");
+ Assert.ok(!!engine, "Should be able to get the new engine");
+
+ Assert.equal(
+ (await Services.search.getDefault()).name,
+ "Bar",
+ "Should have kept the default engine the same"
+ );
+
+ Assert.deepEqual(
+ (await Services.search.getVisibleEngines()).map(e => e.name),
+ // Expected order: Default, then others in alphabetical.
+ ["engine1", "Bar", "engine2"],
+ "Should have updated the engine order"
+ );
+
+ await extension.unload();
+ await promiseAfterSettings();
+});
+
+add_task(async function test_upgrade_to_existing_name_not_allowed() {
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "engine",
+ search_url_get_params: `q={searchTerms}&version=1.0`,
+ version: "1.0",
+ },
+ { skipUnload: true }
+ );
+
+ let engine = Services.search.getEngineByName("engine");
+ Assert.ok(!!engine, "Should have loaded the engine");
+
+ let promise = AddonTestUtils.waitForSearchProviderStartup(extension);
+ let name = "engine1";
+ consoleAllowList.push(`An engine called ${name} already exists`);
+ let manifest = SearchTestUtils.createEngineManifest({
+ name,
+ search_url_get_params: `q={searchTerms}&version=2.0`,
+ version: "2.0",
+ });
+ await extension.upgrade({
+ useAddonManager: "permanent",
+ manifest,
+ });
+ await promise;
+
+ Assert.equal(
+ Services.search.getEngineByName("engine1").getSubmission("").uri.spec,
+ "https://1.example.com/",
+ "Should have not changed the original engine"
+ );
+
+ console.log((await Services.search.getEngines()).map(e => e.name));
+
+ engine = Services.search.getEngineByName("engine");
+ Assert.ok(!!engine, "Should still be able to get the engine by the old name");
+
+ await extension.unload();
+ await promiseAfterSettings();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_webextensions_valid.js b/toolkit/components/search/tests/xpcshell/test_webextensions_valid.js
new file mode 100644
index 0000000000..b5e71d2560
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_webextensions_valid.js
@@ -0,0 +1,165 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const { promiseShutdownManager, promiseStartupManager } = AddonTestUtils;
+
+let extension;
+let oldRemoveEngineFunc;
+
+add_task(async function setup() {
+ await SearchTestUtils.useTestEngines("simple-engines");
+ await promiseStartupManager();
+
+ Services.telemetry.canRecordExtended = true;
+
+ await Services.search.init();
+ await promiseAfterSettings();
+
+ extension = await SearchTestUtils.installSearchExtension(
+ {},
+ { skipUnload: true }
+ );
+ await extension.awaitStartup();
+
+ // For these tests, stub-out the removeEngine function, so that when we
+ // remove it from the add-on manager, the engine is left in the search
+ // settings.
+ oldRemoveEngineFunc = Services.search.wrappedJSObject.removeEngine.bind(
+ Services.search.wrappedJSObject
+ );
+ Services.search.wrappedJSObject.removeEngine = () => {};
+
+ registerCleanupFunction(async () => {
+ await promiseShutdownManager();
+ });
+});
+
+add_task(async function test_valid_extensions_do_nothing() {
+ Services.telemetry.clearScalars();
+
+ Assert.ok(
+ Services.search.getEngineByName("Example"),
+ "Should have installed the engine"
+ );
+
+ await Services.search.runBackgroundChecks();
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+
+ Assert.deepEqual(scalars, {}, "Should not have recorded any issues");
+});
+
+add_task(async function test_different_name() {
+ Services.telemetry.clearScalars();
+
+ let engine = Services.search.getEngineByName("Example");
+
+ engine.wrappedJSObject._name = "Example Test";
+
+ await Services.search.runBackgroundChecks();
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "browser.searchinit.engine_invalid_webextension",
+ extension.id,
+ 5
+ );
+
+ engine.wrappedJSObject._name = "Example";
+});
+
+add_task(async function test_different_url() {
+ Services.telemetry.clearScalars();
+
+ let engine = Services.search.getEngineByName("Example");
+
+ engine.wrappedJSObject._urls = [];
+ engine.wrappedJSObject._setUrls({
+ search_url: "https://example.com/123",
+ search_url_get_params: "?q={searchTerms}",
+ });
+
+ await Services.search.runBackgroundChecks();
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "browser.searchinit.engine_invalid_webextension",
+ extension.id,
+ 6
+ );
+});
+
+add_task(async function test_extension_no_longer_specifies_engine() {
+ Services.telemetry.clearScalars();
+
+ let extensionInfo = {
+ useAddonManager: "permanent",
+ manifest: {
+ version: "2.0",
+ browser_specific_settings: {
+ gecko: {
+ id: "example@tests.mozilla.org",
+ },
+ },
+ },
+ };
+
+ await extension.upgrade(extensionInfo);
+
+ await Services.search.runBackgroundChecks();
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "browser.searchinit.engine_invalid_webextension",
+ extension.id,
+ 4
+ );
+});
+
+add_task(async function test_disabled_extension() {
+ // We don't clear scalars between tests to ensure the scalar gets set
+ // to the new value, rather than added.
+
+ // Disable the extension, this won't remove the search engine because we've
+ // stubbed removeEngine.
+ await extension.addon.disable();
+
+ await Services.search.runBackgroundChecks();
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "browser.searchinit.engine_invalid_webextension",
+ extension.id,
+ 2
+ );
+
+ extension.addon.enable();
+ await extension.awaitStartup();
+});
+
+add_task(async function test_missing_extension() {
+ // We don't clear scalars between tests to ensure the scalar gets set
+ // to the new value, rather than added.
+
+ let extensionId = extension.id;
+ // Remove the extension, this won't remove the search engine because we've
+ // stubbed removeEngine.
+ await extension.unload();
+
+ await Services.search.runBackgroundChecks();
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "browser.searchinit.engine_invalid_webextension",
+ extensionId,
+ 1
+ );
+
+ await oldRemoveEngineFunc(Services.search.getEngineByName("Example"));
+});
diff --git a/toolkit/components/search/tests/xpcshell/xpcshell.ini b/toolkit/components/search/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..4f0be19aa6
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,196 @@
+[DEFAULT]
+firefox-appdir = browser
+head = head_search.js
+dupe-manifest =
+tags=searchmain
+skip-if = toolkit == 'android'
+prefs =
+ browser.search.removeEngineInfobar.enabled=true
+
+support-files =
+ data/engine.xml
+ data/engine/manifest.json
+ data/engine2.xml
+ data/engine2/manifest.json
+ data/engine-app/manifest.json
+ data/engine-diff-name/manifest.json
+ data/engine-diff-name/_locales/en/messages.json
+ data/engine-diff-name/_locales/gd/messages.json
+ data/engine-fr.xml
+ data/engine-fr/manifest.json
+ data/engine-reordered/manifest.json
+ data/engineMaker.sjs
+ data/engine-pref/manifest.json
+ data/engine-rel-searchform-purpose/manifest.json
+ data/engine-system-purpose/manifest.json
+ data/engineImages.xml
+ data/engine-chromeicon/manifest.json
+ data/engine-purposes/manifest.json
+ data/engine-resourceicon/manifest.json
+ data/engine-resourceicon/_locales/en/messages.json
+ data/engine-resourceicon/_locales/gd/messages.json
+ data/engine-same-name/manifest.json
+ data/engine-same-name/_locales/en/messages.json
+ data/engine-same-name/_locales/gd/messages.json
+ data/engines-no-order-hint.json
+ data/engines.json
+ data/iconsRedirect.sjs
+ data/search.json
+ data/search-legacy.json
+ data/search-legacy-correct-default-engine-hashes.json
+ data/search-legacy-no-ids.json
+ data/search-legacy-old-loadPaths.json
+ data/search-legacy-wrong-default-engine-hashes.json
+ data/search-legacy-wrong-third-party-engine-hashes.json
+ data/search-obsolete-app.json
+ data/search-obsolete-distribution.json
+ data/search-obsolete-langpack.json
+ data/searchSuggestions.sjs
+ data/geolookup-extensions/multilocale/favicon.ico
+ data/geolookup-extensions/multilocale/manifest.json
+ data/geolookup-extensions/multilocale/_locales/af/messages.json
+ data/geolookup-extensions/multilocale/_locales/an/messages.json
+ data1/engine1/manifest.json
+ data1/engine2/manifest.json
+ data1/exp2/manifest.json
+ data1/exp3/manifest.json
+ data1/engines.json
+ simple-engines/engines.json
+ simple-engines/basic/manifest.json
+ simple-engines/hidden/manifest.json
+ simple-engines/simple/manifest.json
+ test-extensions/engines.json
+ test-extensions/plainengine/favicon.ico
+ test-extensions/plainengine/manifest.json
+ test-extensions/special-engine/favicon.ico
+ test-extensions/special-engine/manifest.json
+ test-extensions/multilocale/favicon-af.ico
+ test-extensions/multilocale/favicon-an.ico
+ test-extensions/multilocale/manifest.json
+ test-extensions/multilocale/_locales/af/messages.json
+ test-extensions/multilocale/_locales/an/messages.json
+
+[test_async.js]
+[test_config_attribution.js]
+[test_config_engine_params.js]
+support-files =
+ method-extensions/get/manifest.json
+ method-extensions/post/manifest.json
+ method-extensions/engines.json
+[test_defaultEngine_fallback.js]
+[test_defaultEngine_experiments.js]
+[test_defaultEngine.js]
+[test_defaultPrivateEngine.js]
+[test_engine_alias.js]
+[test_engine_ids.js]
+[test_engine_multiple_alias.js]
+[test_engine_selector_application_distribution.js]
+[test_engine_selector_application_name.js]
+[test_engine_selector_application.js]
+[test_engine_selector_order.js]
+[test_engine_selector_override.js]
+[test_engine_selector_remote_settings.js]
+tag = remotesettings searchmain
+[test_engine_selector.js]
+[test_engine_set_alias.js]
+[test_getSubmission_encoding.js]
+[test_getSubmission_params.js]
+[test_identifiers.js]
+[test_ignorelist_update.js]
+[test_ignorelist.js]
+[test_initialization.js]
+[test_initialization_with_region.js]
+[test_list_json_locale.js]
+[test_list_json_no_private_default.js]
+[test_list_json_searchdefault.js]
+[test_list_json_searchorder.js]
+[test_maybereloadengine_order.js]
+[test_migrateWebExtensionEngine.js]
+[test_missing_engine.js]
+[test_multipleIcons.js]
+[test_nodb_pluschanges.js]
+[test_notifications.js]
+[test_opensearch_icon.js]
+support-files =
+ data/bigIcon.ico
+ data/remoteIcon.ico
+ data/svgIcon.svg
+[test_opensearch_icons_invalid.js]
+support-files =
+ opensearch/chromeicon.xml
+ opensearch/resourceicon.xml
+[test_opensearch_install_errors.js]
+support-files = opensearch/invalid.xml
+[test_opensearch_telemetry.js]
+support-files =
+ opensearch/secure-and-securely-updated1.xml
+ opensearch/secure-and-securely-updated2.xml
+ opensearch/secure-and-securely-updated3.xml
+ opensearch/secure-and-securely-updated-insecure-form.xml
+ opensearch/secure-and-insecurely-updated1.xml
+ opensearch/secure-and-insecurely-updated2.xml
+ opensearch/insecure-and-securely-updated1.xml
+ opensearch/insecure-and-insecurely-updated1.xml
+ opensearch/insecure-and-insecurely-updated2.xml
+ opensearch/secure-and-no-update-url1.xml
+ opensearch/insecure-and-no-update-url1.xml
+ opensearch/secure-localhost.xml
+ opensearch/secure-onionv2.xml
+ opensearch/secure-onionv3.xml
+[test_opensearch_update.js]
+[test_opensearch.js]
+support-files =
+ opensearch/mozilla-ns.xml
+ opensearch/post.xml
+ opensearch/simple.xml
+ opensearch/suggestion.xml
+ opensearch/suggestion-alternate.xml
+[test_appDefaultEngine.js]
+[test_override_allowlist.js]
+[test_parseSubmissionURL.js]
+[test_policyEngine.js]
+[test_pref.js]
+skip-if = nightly_build
+[test_purpose.js]
+[test_reload_engines_experiment.js]
+[test_reload_engines_locales.js]
+[test_reload_engines.js]
+[test_remove_profile_engine.js]
+[test_remove_engine_notification_box.js]
+[test_searchUrlDomain.js]
+[test_save_sorted_engines.js]
+[test_SearchStaticData.js]
+[test_searchSuggest_cookies.js]
+[test_searchSuggest_extraParams.js]
+[test_searchSuggest_private.js]
+[test_searchSuggest.js]
+[test_searchTermFromResult.js]
+[test_selectedEngine.js]
+[test_sendSubmissionURL.js]
+[test_settings_broken.js]
+[test_settings_duplicate.js]
+[test_settings_good.js]
+[test_settings_ignorelist.js]
+support-files = data/search_ignorelist.json
+[test_settings_migration_ids.js]
+[test_settings_migration_loadPath.js]
+[test_settings_none.js]
+[test_settings_obsolete.js]
+[test_settings_persist_diff_locale_same_name.js]
+[test_settings_persist.js]
+[test_settings.js]
+[test_sort_orders-no-hints.js]
+[test_sort_orders.js]
+[test_telemetry_event_default.js]
+[test_userEngine.js]
+[test_validate_engines.js]
+[test_validate_manifests.js]
+[test_webextensions_builtin_upgrade.js]
+[test_webextensions_install.js]
+[test_webextensions_language_switch.js]
+[test_webextensions_migrate_to.js]
+support-files = data/search-migration.json
+[test_webextensions_normandy_upgrade.js]
+[test_webextensions_startup_remove.js]
+[test_webextensions_upgrade.js]
+[test_webextensions_valid.js]