From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../search/tests/xpcshell/head_search.js | 512 +++++++++++++++++++++ 1 file changed, 512 insertions(+) create mode 100644 toolkit/components/search/tests/xpcshell/head_search.js (limited to 'toolkit/components/search/tests/xpcshell/head_search.js') 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 + ); + } + } + } +}); -- cgit v1.2.3