diff options
Diffstat (limited to 'toolkit/components/search/tests/xpcshell/searchconfigs')
19 files changed, 2528 insertions, 0 deletions
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..72c4d4f04f --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/head_searchconfig.js @@ -0,0 +1,604 @@ +/* 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, { + // Only needed when SearchUtils.newSearchConfigEnabled is false. + AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs", + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + ObjectUtils: "resource://gre/modules/ObjectUtils.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", + SearchEngineSelectorOld: + "resource://gre/modules/SearchEngineSelectorOld.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", + updateAppInfo: "resource://testing-common/AppInfo.sys.mjs", +}); + +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") { + if (SearchUtils.newSearchConfigEnabled) { + updateAppInfo({ + name: "XPCShell", + ID: "xpcshell@tests.mozilla.org", + version, + platformVersion: version, + }); + } else { + 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 + ); + + if (!SearchUtils.newSearchConfigEnabled) { + 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 || + SearchUtils.newSearchConfigEnabled + ? new SearchEngineSelector() + : new SearchEngineSelectorOld(); + + // 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 configs = await engineSelector.fetchEngineConfiguration({ + locale, + region: region || "default", + channel: SearchUtils.MODIFIED_APP_CHANNEL, + }); + + return SearchTestUtils.searchConfigToEngines(configs.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/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 || locales.includes(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); + } + } +} + +async function checkUISchemaValid(configSchema, uiSchema) { + for (let key of Object.keys(configSchema.properties)) { + Assert.ok( + uiSchema["ui:order"].includes(key), + `Should have ${key} listed at the top-level of the ui schema` + ); + } +} 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..bfe05e1596 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_amazon.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +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: ["us", "jp"], + }, + ], + }, + details: [ + { + domain: "amazon.co.jp", + telemetryId: "amazon-jp", + aliases: ["@amazon"], + included: [ + { + regions: ["jp"], + }, + ], + searchUrlCode: "tag=mozillajapan-fx-22", + noSuggestionsURL: true, + }, + { + domain: "amazon.com", + telemetryId: "amazondotcom-us-adm", + aliases: ["@amazon"], + included: [ + { + regions: ["us"], + }, + ], + noSuggestionsURL: true, + searchUrlCode: "tag=admarketus-20", + }, + ], +}); + +add_setup(async function () { + // 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( + { skip_if: () => SearchUtils.newSearchConfigEnabled }, + async function test_searchConfig_amazon_pre89() { + const version = "88.0"; + AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + version, + version + ); + // For pre-89, Amazon has a slightly different config. + let details = test._config.details.find( + d => d.telemetryId == "amazondotcom-us-adm" + ); + details.telemetryId = "amazondotcom"; + delete details.searchUrlCode; + + 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..3c66708bdc --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_baidu.js @@ -0,0 +1,39 @@ +/* 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: ["zh-CN"], + }, + ], + }, + available: { + included: [ + { + locales: ["zh-CN"], + }, + ], + }, + details: [ + { + included: [{}], + domain: "baidu.com", + telemetryId: "baidu", + }, + ], +}); + +add_setup(async function () { + 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..6dbddd8a08 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_bing.js @@ -0,0 +1,134 @@ +/* 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: [ + "ach", + "af", + "an", + "ar", + "ast", + "az", + "bn", + "bs", + "ca", + "ca-valencia", + "cak", + "cs", + "cy", + "da", + "de", + "dsb", + "el", + "en-CA", + "en-GB", + "en-US", + "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", + ], + }, + ], + }, + 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_setup(async function () { + 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..513fdaafef --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_distributions.js @@ -0,0 +1,348 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + 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 => + 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", + SearchUtils.newSearchConfigEnabled + ? "https://suggestplugin.gmx.co.uk/s?brand=gmxcouk&origin=moz_splugin_ff&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", + SearchUtils.newSearchConfigEnabled + ? "https://suggestplugin.gmx.es/s?brand=gmxes&origin=moz_splugin_ff&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", + SearchUtils.newSearchConfigEnabled + ? "https://suggestplugin.gmx.fr/s?brand=gmxfr&origin=moz_splugin_ff&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 = SearchUtils.newSearchConfigEnabled + ? new SearchEngineSelector() + : new SearchEngineSelectorOld(); + +add_setup(async function () { + if (!SearchUtils.newSearchConfigEnabled) { + AddonTestUtils.init(GLOBAL_SCOPE); + AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" + ); + 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..ffbd3fb1ce --- /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_setup(async function () { + 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..ed8e5147ee --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_ebay.js @@ -0,0 +1,276 @@ +/* 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: [ + "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: ["en-US"], + }, + { + regions: ["gb"], + locales: ["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: ["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: ["de", "dsb", "hsb"], + }, + ], + searchUrlCode: "mkrid=5221-53469-19255-0", + suggestUrlCode: "sId=16", + }, + { + domain: "www.ebay.ca", + telemetryId: "ebay-ca", + included: [ + { + locales: DOMAIN_LOCALES["ebay-ca"], + }, + { + regions: ["ca"], + }, + ], + excluded: [ + { + locales: [ + ...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: DOMAIN_LOCALES["ebay-ch"], + }, + { + regions: ["ch"], + }, + ], + excluded: [ + { + locales: [ + ...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: ["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: ["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: DOMAIN_LOCALES["ebay-ie"], + }, + { + regions: ["ie"], + locales: ["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: DOMAIN_LOCALES["ebay-uk"], + }, + { + locales: ["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: 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: DOMAIN_LOCALES["ebay-es"], + }, + ], + searchUrlCode: "mkrid=1185-53479-19255-0", + suggestUrlCode: "sId=186", + }, + { + domain: "www.ebay.fr", + telemetryId: "ebay-fr", + included: [ + { + locales: ["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: DOMAIN_LOCALES["ebay-it"], + }, + ], + searchUrlCode: "mkrid=724-53478-19255-0", + suggestUrlCode: "sId=101", + }, + { + domain: "www.ebay.nl", + telemetryId: "ebay-nl", + included: [ + { + locales: DOMAIN_LOCALES["ebay-nl"], + }, + { + locales: ["unknown", "en-US"], + regions: ["nl"], + }, + ], + excluded: [{ regions: ["be"] }], + searchUrlCode: "mkrid=1346-53482-19255-0", + suggestUrlCode: "sId=146", + }, + ], +}); + +add_setup(async function () { + 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..e9fc7241b1 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_ecosia.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: "ecosia", + aliases: [], + default: { + // Not default anywhere. + }, + available: { + included: [ + { + locales: ["de"], + }, + ], + }, + details: [ + { + included: [{}], + domain: "www.ecosia.org", + telemetryId: "ecosia", + searchUrlCode: "tt=mzl", + }, + ], +}); + +add_setup(async function () { + 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..c6b9d6a991 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_google.js @@ -0,0 +1,171 @@ +/* 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: ["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_setup(async function () { + 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..e1a9c66b12 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_mailru.js @@ -0,0 +1,36 @@ +/* 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: ["ru"], + }, + ], + }, + details: [ + { + included: [{}], + domain: "go.mail.ru", + telemetryId: "mailru", + codes: "gp=900200", + searchUrlCode: "frc=900200", + }, + ], +}); + +add_setup(async function () { + 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..4024385729 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_qwant.js @@ -0,0 +1,36 @@ +/* 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: ["fr"], + }, + ], + }, + details: [ + { + included: [{}], + domain: "www.qwant.com", + telemetryId: "qwant", + searchUrlCode: "client=brz-moz", + suggestUrlCode: "client=opensearch", + }, + ], +}); + +add_setup(async function () { + 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..2c0010e2b3 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_rakuten.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: "rakuten", + aliases: [], + default: { + // Not default anywhere. + }, + available: { + included: [ + { + locales: ["ja", "ja-JP-macos"], + }, + ], + }, + details: [ + { + included: [{}], + domain: "rakuten.co.jp", + telemetryId: "rakuten", + searchUrlCodeNotInQuery: "013ca98b.cd7c5f0c", + }, + ], +}); + +add_setup(async function () { + await test.setup(); +}); + +add_task(async function test_searchConfig_rakuten() { + await test.run(); +}); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchconfig_validates.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchconfig_validates.js new file mode 100644 index 0000000000..51e71ff573 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchconfig_validates.js @@ -0,0 +1,209 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs", + SearchEngineSelectorOld: + "resource://gre/modules/SearchEngineSelectorOld.sys.mjs", +}); + +/** + * Checks to see if a value is an object or not. + * + * @param {*} value + * The value to check. + * @returns {boolean} + */ +function isObject(value) { + return value != null && typeof value == "object" && !Array.isArray(value); +} + +/** + * This function modifies the schema to prevent allowing additional properties + * on objects. This is used to enforce that the schema contains everything that + * we deliver via the search configuration. + * + * These checks are not enabled in-product, as we want to allow older versions + * to keep working if we add new properties for whatever reason. + * + * @param {object} section + * The section to check to see if an additionalProperties flag should be added. + */ +function disallowAdditionalProperties(section) { + // It is generally acceptable for new properties to be added to the + // configuration as older builds will ignore them. + // + // As a result, we only check for new properties on nightly builds, and this + // avoids us having to uplift schema changes. This also helps preserve the + // schemas as documentation of "what was supported in this version". + if (!AppConstants.NIGHTLY_BUILD) { + return; + } + + // If the section is a `oneOf` section, avoid the additionalProperties check. + // Otherwise, the validator expects all properties of any `oneOf` item to be + // present. + if (isObject(section)) { + if (section.properties && !("recordType" in section.properties)) { + section.additionalProperties = false; + } + if ("then" in section) { + section.then.additionalProperties = false; + } + } + + for (let value of Object.values(section)) { + if (isObject(value)) { + disallowAdditionalProperties(value); + } else if (Array.isArray(value)) { + for (let item of value) { + disallowAdditionalProperties(item); + } + } + } +} + +let searchConfigSchemaV1; +let searchConfigSchema; + +add_setup(async function () { + searchConfigSchemaV1 = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, "search-config-schema.json") + ); + searchConfigSchema = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, "search-config-v2-schema.json") + ); +}); + +async function checkSearchConfigValidates(schema, searchConfig) { + disallowAdditionalProperties(schema); + let validator = new JsonSchema.Validator(schema); + + for (let entry of searchConfig) { + // Records in Remote Settings contain additional properties independent of + // the schema. Hence, we don't want to validate their presence. + delete entry.schema; + delete entry.id; + delete entry.last_modified; + + let result = validator.validate(entry); + // entry.webExtension.id supports search-config v1. + let message = `Should validate ${ + entry.identifier ?? entry.recordType ?? entry.webExtension.id + }`; + if (!result.valid) { + message += `:\n${JSON.stringify(result.errors, null, 2)}`; + } + Assert.ok(result.valid, message); + + // All engine objects should have the base URL defined for each entry in + // entry.base.urls. + // Unfortunately this is difficult to enforce in the schema as it would + // need a `required` field that works across multiple levels. + if (entry.recordType == "engine") { + for (let urlEntry of Object.values(entry.base.urls)) { + Assert.ok( + urlEntry.base, + "Should have a base url for every URL defined on the top-level base object." + ); + } + } + } +} + +async function checkSearchConfigOverrideValidates( + schema, + searchConfigOverride +) { + let validator = new JsonSchema.Validator(schema); + + for (let entry of searchConfigOverride) { + // Records in Remote Settings contain additional properties independent of + // the schema. Hence, we don't want to validate their presence. + delete entry.schema; + delete entry.id; + delete entry.last_modified; + + let result = validator.validate(entry); + + let message = `Should validate ${entry.identifier ?? entry.telemetryId}`; + if (!result.valid) { + message += `:\n${JSON.stringify(result.errors, null, 2)}`; + } + Assert.ok(result.valid, message); + } +} + +add_task(async function test_search_config_validates_to_schema_v1() { + let selector = new SearchEngineSelectorOld(() => {}); + let searchConfig = await selector.getEngineConfiguration(); + + await checkSearchConfigValidates(searchConfigSchemaV1, searchConfig); +}); + +add_task(async function test_ui_schema_valid_v1() { + let uiSchema = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, "search-config-ui-schema.json") + ); + + await checkUISchemaValid(searchConfigSchemaV1, uiSchema); +}); + +add_task(async function test_search_config_override_validates_to_schema_v1() { + let selector = new SearchEngineSelectorOld(() => {}); + let searchConfigOverrides = await selector.getEngineConfigurationOverrides(); + let overrideSchema = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, "search-config-overrides-schema.json") + ); + + await checkSearchConfigOverrideValidates( + overrideSchema, + searchConfigOverrides + ); +}); + +add_task( + { skip_if: () => !SearchUtils.newSearchConfigEnabled }, + async function test_search_config_validates_to_schema() { + delete SearchUtils.newSearchConfigEnabled; + SearchUtils.newSearchConfigEnabled = true; + + let selector = new SearchEngineSelector(() => {}); + let searchConfig = await selector.getEngineConfiguration(); + + await checkSearchConfigValidates(searchConfigSchema, searchConfig); + } +); + +add_task( + { skip_if: () => !SearchUtils.newSearchConfigEnabled }, + async function test_ui_schema_valid() { + let uiSchema = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, "search-config-v2-ui-schema.json") + ); + + await checkUISchemaValid(searchConfigSchema, uiSchema); + } +); + +add_task( + { skip_if: () => !SearchUtils.newSearchConfigEnabled }, + async function test_search_config_override_validates_to_schema() { + let selector = new SearchEngineSelector(() => {}); + let searchConfigOverrides = + await selector.getEngineConfigurationOverrides(); + let overrideSchema = await IOUtils.readJSON( + PathUtils.join( + do_get_cwd().path, + "search-config-overrides-v2-schema.json" + ) + ); + + await checkSearchConfigOverrideValidates( + overrideSchema, + searchConfigOverrides + ); + } +); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchicons_validates.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchicons_validates.js new file mode 100644 index 0000000000..c830bb7ade --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchicons_validates.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let searchIconsSchema; + +add_setup(async function () { + searchIconsSchema = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, "search-config-icons-schema.json") + ); +}); + +add_task(async function test_ui_schema_valid() { + let uiSchema = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, "search-config-icons-ui-schema.json") + ); + + await checkUISchemaValid(searchIconsSchema, uiSchema); +}); 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..9bd032c3b8 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_selector_db_out_of_date.js @@ -0,0 +1,93 @@ +/* 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", +}); + +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. + if (SearchUtils.newSearchConfigEnabled) { + await RemoteSettingsWorker._execute("_test_only_import", [ + "main", + SearchUtils.SETTINGS_KEY, + [ + { + id: "b70edfdd-1c3f-4b7b-ab55-38cb048636c0", + identifier: "outofdate", + recordType: "engine", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + last_modified: 1606227264000, + }, + ], + 1606227264000, + ]); + } else { + 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 = SearchUtils.newSearchConfigEnabled + ? new SearchEngineSelector() + : new SearchEngineSelectorOld(); + + let result = await engineSelector.fetchEngineConfiguration({ + // Use the fallback default locale/regions to get a simple list. + locale: "default", + region: "default", + }); + + if (SearchUtils.newSearchConfigEnabled) { + Assert.deepEqual( + result.engines.map(e => e.identifier), + ["google", "ddg", "wikipedia"], + "Should have returned the correct data." + ); + } else { + 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_wikipedia.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_wikipedia.js new file mode 100644 index 0000000000..54cf764830 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_wikipedia.js @@ -0,0 +1,171 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const testConfiguration = { + identifier: "wikipedia", + default: { + // Not default anywhere. + }, + available: { + excluded: [ + // Should be available everywhere. + ], + }, + details: [ + // Details generated below. + ], +}; + +/** + * Generates the expected details for the given locales and inserts + * them into the testConfiguration. + * + * @param {string[]} locales + * The locales for this details entry - which locales this variant of + * Wikipedia is expected to be deployed to. + * @param {string} [subDomainName] + * The expected sub domain name for this variant of Wikipedia. If not + * specified, defaults to the first item in the locales array. + * @param {string} [telemetrySuffix] + * The expected suffix used when this variant is reported via telemetry. If + * not specified, defaults to the first item in the array. If this is the + * empty string, then it "wikipedia" (i.e. no suffix) will be the expected + * value. + */ +function generateExpectedDetails(locales, subDomainName, telemetrySuffix) { + if (!subDomainName) { + subDomainName = locales[0]; + } + if (telemetrySuffix == undefined) { + telemetrySuffix = locales[0]; + } + testConfiguration.details.push({ + domain: `${subDomainName}.wikipedia.org`, + telemetryId: telemetrySuffix ? `wikipedia-${telemetrySuffix}` : "wikipedia", + aliases: ["@wikipedia"], + included: [{ locales }], + }); +} + +// This is an array of an array of arguments to be passed to generateExpectedDetails(). +// These are the locale, sub domain name and telemetry id suffix expectations for +// the test to check. +// Note that the expectations for en.wikipedia.com are generated in add_setup. +const LOCALES_INFO = [ + [["af"]], + [["an"]], + [["ar"]], + [["ast"]], + [["az"]], + [["be"]], + [["bg"]], + [["bn"]], + [["br"]], + [["bs"]], + [["ca", "ca-valencia"], "ca", "ca"], + [["cs"], "cs", "cz"], + [["cy"]], + [["da"]], + [["de"]], + [["dsb"]], + [["el"]], + [["eo"]], + [["cak", "es-AR", "es-CL", "es-ES", "es-MX", "trs"], "es", "es"], + [["et"]], + [["eu"]], + [["fa"]], + [["fi"]], + [["fr", "ff", "son"], "fr", "fr"], + [["fy-NL"], "fy", "fy-NL"], + [["ga-IE"], "ga", "ga-IE"], + [["gd"]], + [["gl"]], + [["gn"]], + [["gu-IN"], "gu", "gu"], + [["hi-IN"], "hi", "hi"], + [["he"]], + [["hr"]], + [["hsb"]], + [["hu"]], + [["hy-AM"], "hy", "hy"], + [["ia"]], + [["id"]], + [["is"]], + [["ja", "ja-JP-macos"], "ja", "ja"], + [["ka"]], + [["kab"]], + [["kk"]], + [["km"]], + [["kn"]], + [["ko"], "ko", "kr"], + [["it", "fur", "sc"], "it", "it"], + [["lij"]], + [["lo"]], + [["lt"]], + [["ltg"]], + [["lv"]], + [["mk"]], + [["mr"]], + [["ms"]], + [["my"]], + [["nb-NO"], "no", "NO"], + [["ne-NP"], "ne", "ne"], + [["nl"]], + [["nn-NO"], "nn", "NN"], + [["oc"]], + [["pa-IN"], "pa", "pa"], + [["pl", "szl"], "pl", "pl"], + [["pt-BR", "pt-PT"], "pt", "pt"], + [["rm"]], + [["ro"]], + [["ru"]], + [["si"]], + [["sk"]], + [["sl"]], + [["sq"]], + [["sr"]], + [["sv-SE"], "sv", "sv-SE"], + [["ta"]], + [["te"]], + [["th"]], + [["tl"]], + [["tr"]], + [["uk"]], + [["ur"]], + [["uz"]], + [["vi"]], + [["wo"]], + [["zh-CN"], "zh", "zh-CN"], + [["zh-TW"], "zh", "zh-TW"], +]; + +const test = new SearchConfigTest(testConfiguration); + +add_setup(async function () { + const allLocales = await test.getLocales(); + + // For the "en" version of Wikipedia, we ship it to all locales where other + // Wikipedias are not shipped. We form the list based on all-locales to avoid + // needing to update the test whenever all-locales is updated. + let enLocales = []; + for (let locale of allLocales) { + if (!LOCALES_INFO.find(d => d[0].includes(locale))) { + enLocales.push(locale); + } + } + + console.log("en.wikipedia.org expected locales are:", enLocales); + generateExpectedDetails(enLocales, "en", ""); + + for (let details of LOCALES_INFO) { + generateExpectedDetails(...details); + } + + await test.setup(); +}); + +add_task(async function test_searchConfig_wikipedia() { + await test.run(); +}); 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..2b80dffd17 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_yahoojp.js @@ -0,0 +1,36 @@ +/* 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: ["ja", "ja-JP-macos"], + }, + ], + }, + details: [ + { + included: [{}], + domain: "search.yahoo.co.jp", + telemetryId: "yahoo-jp", + searchUrlCode: "fr=mozff", + }, + ], +}); + +add_setup(async function () { + 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..6e2575b3c3 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_yandex.js @@ -0,0 +1,110 @@ +/* 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: ["ru", "tr", "be", "kk", "en-CA", "en-GB", "en-US"], + }, + ], + }, + available: { + included: [ + { + locales: ["az", "ru", "be", "kk", "tr"], + }, + { + regions: ["ru", "tr", "by", "kz"], + locales: ["en-CA", "en-GB", "en-US"], + }, + ], + }, + details: [ + { + included: [{ locales: ["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: ["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: ["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: ["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: ["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_setup(async function () { + await test.setup(); +}); + +add_task(async function test_searchConfig_yandex() { + await test.run(); +}).skip(); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/xpcshell.toml b/toolkit/components/search/tests/xpcshell/searchconfigs/xpcshell.toml new file mode 100644 index 0000000000..8baff2a38d --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/xpcshell.toml @@ -0,0 +1,65 @@ +[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 = [ + "os == '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_searchconfig_validates.js"] +support-files = [ + "../../../schema/search-config-overrides-schema.json", + "../../../schema/search-config-overrides-v2-schema.json", + "../../../schema/search-config-schema.json", + "../../../schema/search-config-ui-schema.json", + "../../../schema/search-config-v2-schema.json", + "../../../schema/search-config-v2-ui-schema.json", +] + +["test_searchicons_validates.js"] +support-files = [ + "../../../schema/search-config-icons-schema.json", + "../../../schema/search-config-icons-ui-schema.json", +] + +["test_selector_db_out_of_date.js"] + +["test_wikipedia.js"] + +["test_yahoojp.js"] + +["test_yandex.js"] |