Source code
Revision control
Copy as Markdown
Other Tools
/* Any copyright is dedicated to the Public Domain.
"use strict";
const { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
);
ChromeUtils.defineESModuleGetters(this, {
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: "moz-src:///toolkit/components/search/SearchEngine.sys.mjs",
SearchEngineSelector:
"moz-src:///toolkit/components/search/SearchEngineSelector.sys.mjs",
SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs",
SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs",
sinon: "resource://testing-common/Sinon.sys.mjs",
updateAppInfo: "resource://testing-common/AppInfo.sys.mjs",
});
/**
* @import {AppProvidedConfigEngine} from "moz-src:///toolkit/components/search/ConfigSearchEngine.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 ENGINES_URLS = {
"prod-main":
"prod-preview":
"stage-main":
"stage-preview":
};
/**
* 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 ENGINES_URLS)) {
throw new Error(`Invalid value for SEARCH_CONFIG`);
}
const url = ENGINES_URLS[SEARCH_CONFIG];
const response = await fetch(url);
const config = await response.json();
SearchTestUtils.setRemoteSettingsConfig(config.data);
}
}
/**
* @typedef {object} RegionLocaleDetails
* @property {string[]} [regions]
* The regions where the search engine should be available. Regions are typically
* displayed as lower case. If no regions are specified, it is assumed that
* any region will match.
* @property {string[]} [locales]
* The locales where the search engine should be available. If no locales are
* specified, it is assumed that any locales will match.
*/
/**
* @typedef {object} DeploymentDetails
* Where the search engine should be deployed to.
*
* If neither included nor excluded are specified, the search engine should
* not be available anywhere.
*
* If excluded is specified and included is not specified, the search engine
* should be available everywhere.
*
* @property {RegionLocaleDetails[]} [included]
* Where the search engine should be available.
* @property {RegionLocaleDetails[]} [excluded]
* Where the search engine should not be available.
*/
/**
* @typedef {object} EngineRuleDetails
* @property {string} domain
* The expected domain of the search URL.
* @property {string} telemetryId
* The expected telemetry ID of the search engine. This is deprecated, but
* we still test it.
* @property {string} [partnerCode]
* The expected partner code property of the search engine.
* @property {string} [searchUrlCode]
* The expected parameter name and value for the partner code in the search URL.
* @property {string} [searchUrlParamNotInQuery]
* When specified, this parameter should not be included in the query part of
* the search URL.
* @property {string} [suggestUrlCode]
* Expected parameter name and value within the suggestion URL, e.g. for checking
* regional parameters.
* @property {string[]} [aliases]
* The expected aliases for the search engine.
* @property {string[]} [required_aliases]
* Required aliases for the search engine. These aliases must be associated
* with the engine, but additional aliases may also be supplied.
* @property {boolean} [noSuggestionsURL]
* Set to true when there are no suggestions available for this search engine.
*/
/**
* @typedef {object} SearchConfigTestDetails
* Details for the search configuration tests for a single engine.
* @property {string} [identifier]
* The identifier for the search engine under test. If not specified then
* `identifierStartsWith` must be specified.
* @property {string} [identifierStartsWith]
* The prefix of the identifier of the search engine under test. This is used
* for search engines that have different set ups per locale/region and have
* different identifiers as a result, e.g. wikipedia, eBay.
* @property {string[]} [aliases]
* The expected aliases for the search engine.
* @property {DeploymentDetails} default
* Details of where the engine should be listed as default.
* @property {DeploymentDetails} available
* Details of where the engine should be listed as available.
* @property {string} [suggestionUrlBase]
* The base URL for search suggestion lookup.
* @property {boolean} [noSuggestionsURL]
* Set to true when there are no suggestions available for this search engine.
* @property {(EngineRuleDetails & DeploymentDetails)[]} details
* Specific details of the test for checking URL details and telemetry information.
*/
/**
* Processes the configuration to get the search engines for the specified
* region/locale.
*
* @param {SearchEngineSelector} engineSelector
* @param {string} region
* @param {string} locale
* @returns {Promise<{engines: AppProvidedConfigEngine[], appDefaultEngineId: string}>}
*/
async function getEngines(engineSelector, region, locale) {
let configs = await engineSelector.fetchEngineConfiguration({
locale,
region,
channel: SearchUtils.MODIFIED_APP_CHANNEL,
});
return {
engines: await SearchTestUtils.searchConfigToEngines(configs.engines),
appDefaultEngineId: configs.appDefaultEngineId,
};
}
/**
* 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.
*/
class SearchConfigTest {
/**
* @type {?SearchEngineSelector}
*/
#engineSelector;
/**
* @type {SearchConfigTestDetails[]}
*/
#testDetails;
/**
* @param {SearchConfigTestDetails[]} testDetails
* An array of configurations for testing multiple engines.
*/
constructor(testDetails) {
this.#testDetails = testDetails;
}
/**
* Sets up the test.
*
* @param {string} [version]
* The version to simulate for running the tests.
*/
async setup(version = "42.0") {
updateAppInfo({
name: "firefox",
ID: "xpcshell@tests.mozilla.org",
version,
platformVersion: version,
});
await maybeSetupConfig();
this.#engineSelector = new SearchEngineSelector();
}
/**
* 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, appDefaultEngineId } = await getEngines(
this.#engineSelector,
region,
locale
);
// Test each configuration in this single iteration
for (let testDetails of this.#testDetails) {
this._assertEngineRules(
engines.filter(e => e.id == appDefaultEngineId),
region,
locale,
"default",
testDetails
);
const isPresent = this._assertAvailableEngines(
region,
locale,
engines,
testDetails
);
if (isPresent) {
this._assertEngineDetails(region, locale, engines, testDetails);
}
}
}
}
}
/**
* @returns {Set<?string>} the list of regions for the tests to run with.
*/
get _regions() {
if (TEST_DEBUG) {
return new Set(["by", "cn", "kz", "us", "ru", "tr", "default"]);
}
return new Set([
...Services.intl.getAvailableLocaleDisplayNames("region"),
"default",
]);
}
/**
* @returns {Promise<string[]>} 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 matches a section of the test details.
*
* @param {RegionLocaleDetails[]} section
* The region/locale details 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 {AppProvidedConfigEngine[]} engines
* The list of engines to check.
* @param {SearchConfigTestDetails} testDetails
* The details of the test.
* @returns {AppProvidedConfigEngine}
* Returns the engine if found, null otherwise.
*/
_findEngine(engines, testDetails) {
return engines.find(engine =>
testDetails.identifier
? engine.id == testDetails.identifier
: engine.id.startsWith(testDetails.identifierStartsWith)
);
}
/**
* Asserts whether the engines rules in the test section are met for the
* associated engine.
*
* @param {AppProvidedConfigEngine[]} engines
* The list of engines to check.
* @param {string} region
* The two-letter region code.
* @param {string} locale
* The two-letter locale code.
* @param {"default" | "available"} section
* The section of the test to check.
* @param {SearchConfigTestDetails} testDetails
* The test details to use.
* @returns {boolean}
* Returns true if the engine is expected to be present, false otherwise.
*/
_assertEngineRules(engines, region, locale, section, testDetails) {
const infoString = `region: "${region}" locale: "${locale}"`;
const testSection = testDetails[section];
const hasIncluded = "included" in testSection;
const hasExcluded = "excluded" in testSection;
const identifierIncluded = !!this._findEngine(engines, testDetails);
// If there's not included/excluded, then this shouldn't be the default anywhere.
if (section == "default" && !hasIncluded && !hasExcluded) {
this.assertOk(
!identifierIncluded,
`${testDetails.identifier} should not be ${section} for any locale/region,
currently set for ${infoString}`
);
return false;
}
// If there's no included section and no excluded, then we assume the
// engine is not available anywhere. If there's no included section, but an
// exluded section we assume it is available everywhere apart from the
// exclusions.
let included =
hasIncluded &&
this._localeRegionInSection(testSection.included, region, locale);
let excluded =
hasExcluded &&
this._localeRegionInSection(testSection.excluded, region, locale);
if (
(included && (!hasExcluded || !excluded)) ||
(!hasIncluded && hasExcluded && !excluded)
) {
this.assertOk(
identifierIncluded,
`${testDetails.identifier} should be ${section} for ${infoString}`
);
return true;
}
this.assertOk(
!identifierIncluded,
`${testDetails.identifier} should not be ${section} for ${infoString}`
);
return false;
}
/**
* 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 {AppProvidedConfigEngine[]} engines
* The current visible engines.
* @param {SearchConfigTestDetails} testDetails
* The test details to use.
* @returns {boolean}
* Returns true if the engine is expected to be present, false otherwise.
*/
_assertAvailableEngines(region, locale, engines, testDetails) {
return this._assertEngineRules(
engines,
region,
locale,
"available",
testDetails
);
}
/**
* Asserts the engine follows various rules.
*
* @param {string} region
* The two-letter region code.
* @param {string} locale
* The two-letter locale code.
* @param {AppProvidedConfigEngine[]} engines
* The current visible engines.
* @param {SearchConfigTestDetails} testDetails
* The test details to use.
*/
_assertEngineDetails(region, locale, engines, testDetails) {
const details = testDetails.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,
`${testDetails.identifier} should have just one details section for region: ${region} locale: ${locale}`
);
const engine = this._findEngine(engines, testDetails);
this.assertOk(
engine,
"${testDetails.identifier} should have an engine present"
);
if (testDetails.aliases) {
this.assertDeepEqual(
engine.aliases,
testDetails.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, testDetails);
this._assertCorrectUrlCode(location, engine, rule);
if ("aliases" in rule) {
this.assertDeepEqual(
engine.aliases,
rule.aliases,
"Should have the correct aliases for the engine"
);
}
if ("required_aliases" in rule) {
this.assertOk(
rule.required_aliases.every(a => engine.aliases.includes(a)),
"Should have the required aliases for the engine"
);
}
if ("telemetryId" in rule) {
this.assertEqual(
engine.telemetryId,
rule.telemetryId,
`Should have the correct telemetryId ${location}.`
);
}
if ("partnerCode" in rule) {
this.assertEqual(
engine.partnerCode,
rule.partnerCode,
`Should have the correct partnerCode ${location}.`
);
}
}
}
/**
* Asserts whether the engine is using the correct domains or not.
*
* @param {string} location
* Debug string with locale + region information.
* @param {AppProvidedConfigEngine} engine
* The engine being tested.
* @param {EngineRuleDetails & DeploymentDetails} rules
* Rules to test.
* @param {SearchConfigTestDetails} testDetails
* The test details to use.
*/
_assertCorrectDomains(location, engine, rules, testDetails) {
this.assertOk(
rules.domain,
`${testDetails.identifier} should have an expectedDomain for the engine ${location}`
);
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 (testDetails.noSuggestionsURL || rules.noSuggestionsURL) {
this.assertOk(!submission, "Should not have a submission url");
} else if (testDetails.suggestionUrlBase) {
this.assertEqual(
submission.uri.prePath + submission.uri.filePath,
testDetails.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 URL codes or not.
*
* @param {string} location
* Debug string with locale + region information.
* @param {AppProvidedConfigEngine} engine
* The engine being tested.
* @param {EngineRuleDetails & DeploymentDetails} 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}"`
);
}
if (rule.searchUrlParamNotInQuery) {
const submission = engine.getSubmission("test", URLTYPE_SEARCH_HTML);
this.assertOk(
!submission.uri.query.includes(rule.searchUrlParamNotInQuery),
`Expected "${rule.searchUrlParamNotInQuery}" should not be 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`
);
}
}