diff options
Diffstat (limited to 'browser/extensions/search-detection/tests')
6 files changed, 527 insertions, 0 deletions
diff --git a/browser/extensions/search-detection/tests/browser/.eslintrc.js b/browser/extensions/search-detection/tests/browser/.eslintrc.js new file mode 100644 index 0000000000..e57058ecb1 --- /dev/null +++ b/browser/extensions/search-detection/tests/browser/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + env: { + webextensions: true, + }, +}; diff --git a/browser/extensions/search-detection/tests/browser/browser.ini b/browser/extensions/search-detection/tests/browser/browser.ini new file mode 100644 index 0000000000..1bd22fe386 --- /dev/null +++ b/browser/extensions/search-detection/tests/browser/browser.ini @@ -0,0 +1,9 @@ +[DEFAULT] +support-files = + redirect.sjs + +[browser_client_side_redirection.js] +[browser_extension_loaded.js] +[browser_server_side_redirection.js] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure diff --git a/browser/extensions/search-detection/tests/browser/browser_client_side_redirection.js b/browser/extensions/search-detection/tests/browser/browser_client_side_redirection.js new file mode 100644 index 0000000000..5dad39dba4 --- /dev/null +++ b/browser/extensions/search-detection/tests/browser/browser_client_side_redirection.js @@ -0,0 +1,204 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +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" }; + +async function testClientSideRedirect({ + background, + permissions, + telemetryExpected = false, +}) { + Services.telemetry.clearEvents(); + + // Load an extension that does a client-side redirect. We expect this + // extension to be reported in a Telemetry event when `telemetryExpected` is + // set to `true`. + const addonId = "some@addon-id"; + const addonVersion = "1.2.3"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + version: addonVersion, + browser_specific_settings: { gecko: { id: addonId } }, + permissions, + }, + useAddonManager: "temporary", + background, + }); + + await extension.startup(); + await extension.awaitMessage("ready"); + + // Simulate a search (with the test search engine) by navigating to it. + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "https://example.com/search?q=babar", + }, + () => {} + ); + + await extension.unload(); + + TelemetryTestUtils.assertEvents( + telemetryExpected + ? [ + { + object: "webrequest", + value: "extension", + extra: { + addonId, + addonVersion, + from: "example.com", + to: "mochi.test", + }, + }, + ] + : [], + TELEMETRY_EVENTS_FILTERS, + TELEMETRY_TEST_UTILS_OPTIONS + ); +} + +add_setup(async function () { + const searchEngineName = "test search engine"; + + let searchEngine; + + // This cleanup function has to be registered before the one registered + // internally by loadExtension, otherwise it is going to trigger a test + // failure (because it will be called too late). + registerCleanupFunction(async () => { + await searchEngine.unload(); + ok( + !Services.search.getEngineByName(searchEngineName), + "test search engine unregistered" + ); + }); + + searchEngine = ExtensionTestUtils.loadExtension({ + manifest: { + chrome_settings_overrides: { + search_provider: { + name: searchEngineName, + keyword: "test", + search_url: "https://example.com/?q={searchTerms}", + }, + }, + }, + // NOTE: the search extension needs to be installed through the + // AddonManager to be correctly unregistered when it is uninstalled. + useAddonManager: "temporary", + }); + + await searchEngine.startup(); + await AddonTestUtils.waitForSearchProviderStartup(searchEngine); + ok( + Services.search.getEngineByName(searchEngineName), + "test search engine registered" + ); +}); + +add_task(function test_onBeforeRequest() { + return testClientSideRedirect({ + background() { + browser.webRequest.onBeforeRequest.addListener( + () => { + return { + redirectUrl: "http://mochi.test:8888/", + }; + }, + { urls: ["*://example.com/*"] }, + ["blocking"] + ); + + browser.test.sendMessage("ready"); + }, + permissions: ["webRequest", "webRequestBlocking", "*://example.com/*"], + telemetryExpected: true, + }); +}); + +add_task(function test_onBeforeRequest_url_not_monitored() { + // Here, we load an extension that does a client-side redirect. Because this + // extension does not listen to the URL of the search engine registered + // above, we don't expect this extension to be reported in a Telemetry event. + return testClientSideRedirect({ + background() { + browser.webRequest.onBeforeRequest.addListener( + () => { + return { + redirectUrl: "http://mochi.test:8888/", + }; + }, + { urls: ["*://google.com/*"] }, + ["blocking"] + ); + + browser.test.sendMessage("ready"); + }, + permissions: ["webRequest", "webRequestBlocking", "*://google.com/*"], + telemetryExpected: false, + }); +}); + +add_task(function test_onHeadersReceived() { + return testClientSideRedirect({ + background() { + browser.webRequest.onHeadersReceived.addListener( + () => { + return { + redirectUrl: "http://mochi.test:8888/", + }; + }, + { urls: ["*://example.com/*"], types: ["main_frame"] }, + ["blocking"] + ); + + browser.test.sendMessage("ready"); + }, + permissions: ["webRequest", "webRequestBlocking", "*://example.com/*"], + telemetryExpected: true, + }); +}); + +add_task(function test_onHeadersReceived_url_not_monitored() { + // Here, we load an extension that does a client-side redirect. Because this + // extension does not listen to the URL of the search engine registered + // above, we don't expect this extension to be reported in a Telemetry event. + return testClientSideRedirect({ + background() { + browser.webRequest.onHeadersReceived.addListener( + () => { + return { + redirectUrl: "http://mochi.test:8888/", + }; + }, + { urls: ["*://google.com/*"], types: ["main_frame"] }, + ["blocking"] + ); + + browser.test.sendMessage("ready"); + }, + permissions: ["webRequest", "webRequestBlocking", "*://google.com/*"], + telemetryExpected: false, + }); +}); diff --git a/browser/extensions/search-detection/tests/browser/browser_extension_loaded.js b/browser/extensions/search-detection/tests/browser/browser_extension_loaded.js new file mode 100644 index 0000000000..65f6ed09a8 --- /dev/null +++ b/browser/extensions/search-detection/tests/browser/browser_extension_loaded.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_searchDetection_isActive() { + let addon = await AddonManager.getAddonByID( + "addons-search-detection@mozilla.com" + ); + + ok(addon, "Add-on exists"); + ok(addon.isActive, "Add-on is active"); + ok(addon.isBuiltin, "Add-on is built-in"); + ok(addon.hidden, "Add-on is hidden"); +}); 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 0000000000..ea235406a4 --- /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.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +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.loadURIString(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.loadURIString(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 0000000000..27cb29b32e --- /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); +} |