diff --git a/browser/extensions/search-detection/tests/browser/browser.ini b/browser/extensions/search-detection/tests/browser/browser.ini index 072018f0dfea5..a5ed547525e6a 100644 --- a/browser/extensions/search-detection/tests/browser/browser.ini +++ b/browser/extensions/search-detection/tests/browser/browser.ini @@ -1,4 +1,7 @@ [DEFAULT] +support-files = + redirect.sjs [browser_client_side_redirection.js] [browser_extension_loaded.js] +[browser_server_side_redirection.js] diff --git a/browser/extensions/search-detection/tests/browser/browser_extension_loaded.js b/browser/extensions/search-detection/tests/browser/browser_extension_loaded.js index c8c5beffa9cb7..88eed3f00cee7 100644 --- a/browser/extensions/search-detection/tests/browser/browser_extension_loaded.js +++ b/browser/extensions/search-detection/tests/browser/browser_extension_loaded.js @@ -1,6 +1,5 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; diff --git a/browser/extensions/search-detection/tests/browser/browser_server_side_redirection.js b/browser/extensions/search-detection/tests/browser/browser_server_side_redirection.js new file mode 100644 index 0000000000000..62be1d907587e --- /dev/null +++ b/browser/extensions/search-detection/tests/browser/browser_server_side_redirection.js @@ -0,0 +1,260 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.import( + "resource://testing-common/AddonTestUtils.jsm" +); + +const { TelemetryTestUtils } = ChromeUtils.import( + "resource://testing-common/TelemetryTestUtils.jsm" +); + +AddonTestUtils.initMochitest(this); + +const TELEMETRY_EVENTS_FILTERS = { + category: "addonsSearchDetection", + method: "etld_change", +}; + +// The search-detection built-in add-on registers dynamic events. +const TELEMETRY_TEST_UTILS_OPTIONS = { clear: true, process: "dynamic" }; + +const REDIRECT_SJS = + "browser/browser/extensions/search-detection/tests/browser/redirect.sjs?q={searchTerms}"; +// This URL will redirect to `example.net`, which is different than +// `*.example.com`. That will be the final URL of a redirect chain: +// www.example.com -> example.net +const SEARCH_URL_WWW = `https://www.example.com/${REDIRECT_SJS}`; +// This URL will redirect to `www.example.com`, which will create a redirect +// chain with two hops: +// test2.example.com -> www.example.com -> example.net +const SEARCH_URL_TEST2 = `https://test2.example.com/${REDIRECT_SJS}`; +// This URL will redirect to `test2.example.com`, which will create a redirect +// chain with three hops: +// test1.example.com -> test2.example.com -> www.example.com -> example.net +const SEARCH_URL_TEST1 = `https://test1.example.com/${REDIRECT_SJS}`; + +const TEST_SEARCH_ENGINE_ADDON_ID = "some@addon-id"; +const TEST_SEARCH_ENGINE_ADDON_VERSION = "4.5.6"; + +const testServerSideRedirect = async ({ + searchURL, + expectedEvents, + tabURL, +}) => { + Services.telemetry.clearEvents(); + + const searchEngineName = "test search engine"; + // Load a default search engine because the add-on we are testing here + // monitors the search engines. + const searchEngine = ExtensionTestUtils.loadExtension({ + manifest: { + version: TEST_SEARCH_ENGINE_ADDON_VERSION, + browser_specific_settings: { + gecko: { id: TEST_SEARCH_ENGINE_ADDON_ID }, + }, + chrome_settings_overrides: { + search_provider: { + name: searchEngineName, + keyword: "test", + search_url: searchURL, + }, + }, + }, + useAddonManager: "temporary", + }); + + await searchEngine.startup(); + ok( + Services.search.getEngineByName(searchEngineName), + "test search engine registered" + ); + await AddonTestUtils.waitForSearchProviderStartup(searchEngine); + + // Simulate a search (with the test search engine) by navigating to it. + const url = tabURL || searchURL.replace("{searchTerms}", "some terms"); + await BrowserTestUtils.withNewTab("about:blank", async browser => { + // Wait for the tab to be fully loaded. + let loaded = BrowserTestUtils.browserLoaded(browser); + BrowserTestUtils.loadURI(browser, url); + await loaded; + }); + + await searchEngine.unload(); + ok( + !Services.search.getEngineByName(searchEngineName), + "test search engine unregistered" + ); + + TelemetryTestUtils.assertEvents( + expectedEvents, + TELEMETRY_EVENTS_FILTERS, + TELEMETRY_TEST_UTILS_OPTIONS + ); +}; + +add_task(function test_redirect_final() { + return testServerSideRedirect({ + // www.example.com -> example.net + searchURL: SEARCH_URL_WWW, + expectedEvents: [ + { + object: "other", + value: "server", + extra: { + addonId: TEST_SEARCH_ENGINE_ADDON_ID, + addonVersion: TEST_SEARCH_ENGINE_ADDON_VERSION, + from: "example.com", + to: "example.net", + }, + }, + ], + }); +}); + +add_task(function test_redirect_two_hops() { + return testServerSideRedirect({ + // test2.example.com -> www.example.com -> example.net + searchURL: SEARCH_URL_TEST2, + expectedEvents: [ + { + object: "other", + value: "server", + extra: { + addonId: TEST_SEARCH_ENGINE_ADDON_ID, + addonVersion: TEST_SEARCH_ENGINE_ADDON_VERSION, + from: "example.com", + to: "example.net", + }, + }, + ], + }); +}); + +add_task(function test_redirect_three_hops() { + return testServerSideRedirect({ + // test1.example.com -> test2.example.com -> www.example.com -> example.net + searchURL: SEARCH_URL_TEST1, + expectedEvents: [ + { + object: "other", + value: "server", + extra: { + addonId: TEST_SEARCH_ENGINE_ADDON_ID, + addonVersion: TEST_SEARCH_ENGINE_ADDON_VERSION, + from: "example.com", + to: "example.net", + }, + }, + ], + }); +}); + +add_task(function test_no_event_when_search_engine_not_used() { + return testServerSideRedirect({ + // www.example.com -> example.net + searchURL: SEARCH_URL_WWW, + // We do not expect any events because the user is not using the search + // engine that was registered. + tabURL: "http://mochi.test:8888/search?q=foobar", + expectedEvents: [], + }); +}); + +add_task(function test_redirect_chain_does_not_start_on_first_request() { + return testServerSideRedirect({ + // www.example.com -> example.net + searchURL: SEARCH_URL_WWW, + // User first navigates to an URL that isn't monitored and will be + // redirected to another URL that is monitored. + tabURL: `http://mochi.test:8888/browser/browser/extensions/search-detection/tests/browser/redirect.sjs?q={searchTerms}`, + expectedEvents: [ + { + object: "other", + value: "server", + extra: { + addonId: TEST_SEARCH_ENGINE_ADDON_ID, + addonVersion: TEST_SEARCH_ENGINE_ADDON_VERSION, + // We expect this and not `mochi.test` because we do not monitor + // `mochi.test`, only `example.com`, which is coming from the search + // engine registered in the test setup. + from: "example.com", + to: "example.net", + }, + }, + ], + }); +}); + +add_task(async function test_two_extensions_reported() { + Services.telemetry.clearEvents(); + + const searchEngines = []; + for (const [addonId, addonVersion, isDefault] of [ + ["1-addon@guid", "1.2", false], + ["2-addon@guid", "3.4", true], + ]) { + const searchEngine = ExtensionTestUtils.loadExtension({ + manifest: { + version: addonVersion, + browser_specific_settings: { + gecko: { id: addonId }, + }, + chrome_settings_overrides: { + search_provider: { + is_default: isDefault, + name: `test search engine - ${addonId}`, + keyword: "test", + search_url: `${SEARCH_URL_WWW}&id=${addonId}`, + }, + }, + }, + useAddonManager: "temporary", + }); + + await searchEngine.startup(); + await AddonTestUtils.waitForSearchProviderStartup(searchEngine); + + searchEngines.push(searchEngine); + } + + // Simulate a search by navigating to it. + const url = SEARCH_URL_WWW.replace("{searchTerms}", "some terms"); + await BrowserTestUtils.withNewTab("about:blank", async browser => { + // Wait for the tab to be fully loaded. + let loaded = BrowserTestUtils.browserLoaded(browser); + BrowserTestUtils.loadURI(browser, url); + await loaded; + }); + + await Promise.all(searchEngines.map(engine => engine.unload())); + + TelemetryTestUtils.assertEvents( + [ + { + object: "other", + value: "server", + extra: { + addonId: "1-addon@guid", + addonVersion: "1.2", + from: "example.com", + to: "example.net", + }, + }, + { + object: "other", + value: "server", + extra: { + addonId: "2-addon@guid", + addonVersion: "3.4", + from: "example.com", + to: "example.net", + }, + }, + ], + TELEMETRY_EVENTS_FILTERS, + TELEMETRY_TEST_UTILS_OPTIONS + ); +}); diff --git a/browser/extensions/search-detection/tests/browser/redirect.sjs b/browser/extensions/search-detection/tests/browser/redirect.sjs new file mode 100644 index 0000000000000..27cb29b32ec26 --- /dev/null +++ b/browser/extensions/search-detection/tests/browser/redirect.sjs @@ -0,0 +1,32 @@ +const REDIRECT_SJS = + "browser/browser/extensions/search-detection/tests/browser/redirect.sjs"; + +// This handler is used to create redirect chains with multiple sub-domains, +// and the next hop is defined by the current `host`. +function handleRequest(request, response) { + let newLocation; + + // test1.example.com -> test2.example.com -> www.example.com -> example.net + switch (request.host) { + case "test1.example.com": + newLocation = `https://test2.example.com/${REDIRECT_SJS}`; + break; + case "test2.example.com": + newLocation = `https://www.example.com/${REDIRECT_SJS}`; + break; + case "www.example.com": + newLocation = "https://example.net/"; + break; + // We redirect `mochi.test` to `www` in + // `test_redirect_chain_does_not_start_on_first_request()`. + case "mochi.test": + newLocation = `https://www.example.com/${REDIRECT_SJS}`; + break; + default: + // Redirect to a different website in case of unexpected events. + newLocation = "https://mozilla.org/"; + } + + response.setStatusLine(request.httpVersion, 302, "Found"); + response.setHeader("Location", newLocation); +}