diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/search/tests | |
parent | Initial commit. (diff) | |
download | firefox-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')
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 Binary files differnew file mode 100644 index 0000000000..f22522411d --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/bigIcon.ico 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&client=firefox&hl={moz:locale}&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 Binary files differnew file mode 100644 index 0000000000..4314071e24 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/favicon.ico 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 Binary files differnew file mode 100644 index 0000000000..442ab4dc80 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/remoteIcon.ico 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 Binary files differnew file mode 100644 index 0000000000..4314071e24 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/favicon-af.ico 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 Binary files differnew file mode 100644 index 0000000000..dda80dfd88 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/favicon-an.ico 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 Binary files differnew file mode 100644 index 0000000000..dda80dfd88 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test-extensions/plainengine/favicon.ico 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 Binary files differnew file mode 100644 index 0000000000..82339b3b1d --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test-extensions/special-engine/favicon.ico 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&ersand"); + + // 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] |