summaryrefslogtreecommitdiffstats
path: root/toolkit/components/search/tests/xpcshell/head_search.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/components/search/tests/xpcshell/head_search.js512
1 files changed, 512 insertions, 0 deletions
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
+ );
+ }
+ }
+ }
+});