diff options
Diffstat (limited to 'toolkit/components/antitracking/bouncetrackingprotection/test')
18 files changed, 1074 insertions, 115 deletions
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml index 1c44d7804e..0e8a01db4a 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml @@ -4,17 +4,31 @@ prefs = [ "privacy.bounceTrackingProtection.enabled=true", "privacy.bounceTrackingProtection.enableTestMode=true", "privacy.bounceTrackingProtection.bounceTrackingPurgeTimerPeriodSec=0", + "privacy.bounceTrackingProtection.enableDryRunMode=false", ] support-files = [ "file_start.html", "file_bounce.sjs", "file_bounce.html", + "file_web_worker.js", ] +["browser_bouncetracking_dry_run.js"] + ["browser_bouncetracking_oa_isolation.js"] +["browser_bouncetracking_popup.js"] + ["browser_bouncetracking_purge.js"] +["browser_bouncetracking_schemes.js"] + ["browser_bouncetracking_simple.js"] -["browser_bouncetracking_stateful.js"] +["browser_bouncetracking_stateful_cookies.js"] + +["browser_bouncetracking_stateful_storage.js"] + +["browser_bouncetracking_stateful_web_worker.js"] + +["browser_bouncetracking_telemetry_purge_duration.js"] diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_dry_run.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_dry_run.js new file mode 100644 index 0000000000..627a5f233a --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_dry_run.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_ORIGIN = "https://itisatracker.org"; +const TEST_BASE_DOMAIN = "itisatracker.org"; + +async function runPurgeTest(expectPurge) { + ok(!SiteDataTestUtils.hasCookies(TEST_ORIGIN), "No cookies initially."); + + // Adding a cookie that should later be purged. + info("Add a test cookie to be purged later."); + SiteDataTestUtils.addToCookies({ origin: TEST_ORIGIN }); + ok(SiteDataTestUtils.hasCookies(TEST_ORIGIN), "Cookie added."); + + // The bounce adds localStorage. Test that there is none initially. + ok( + !SiteDataTestUtils.hasLocalStorage(TEST_ORIGIN), + "No localStorage initially." + ); + + info("Test client bounce with cookie."); + await runTestBounce({ + bounceType: "client", + setState: "localStorage", + skipSiteDataCleanup: true, + postBounceCallback: () => { + info( + "Test that after the bounce but before purging cookies and localStorage are present." + ); + ok(SiteDataTestUtils.hasCookies(TEST_ORIGIN), "Cookies not purged."); + ok( + SiteDataTestUtils.hasLocalStorage(TEST_ORIGIN), + "localStorage not purged." + ); + + Assert.deepEqual( + bounceTrackingProtection + .testGetBounceTrackerCandidateHosts({}) + .map(entry => entry.siteHost), + [TEST_BASE_DOMAIN], + `Bounce tracker candidate '${TEST_BASE_DOMAIN}' added` + ); + }, + }); + + if (expectPurge) { + info("After purging the site shouldn't have any data."); + ok(!SiteDataTestUtils.hasCookies(TEST_ORIGIN), "Cookies purged."); + ok(!SiteDataTestUtils.hasLocalStorage(TEST_ORIGIN), "localStorage purged."); + } else { + info("Purging did not run meaning the site should still have data."); + + ok(SiteDataTestUtils.hasCookies(TEST_ORIGIN), "Cookies still set."); + ok( + SiteDataTestUtils.hasLocalStorage(TEST_ORIGIN), + "localStorage still set." + ); + } + + info( + "Candidates should have been removed after running the purging algorithm. This is true for both regular and dry-run mode where we pretend to purge." + ); + Assert.deepEqual( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}), + [], + "No bounce tracker candidates after purging." + ); + + // Cleanup. + bounceTrackingProtection.clearAll(); + await SiteDataTestUtils.clear(); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", true], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + // Required to use SiteDataTestUtils localStorage helpers. + ["dom.storage.client_validation", false], + ], + }); +}); + +add_task(async function test_purge_in_regular_mode() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.bounceTrackingProtection.enableDryRunMode", false]], + }); + + await runPurgeTest(true); +}); + +add_task(async function test_purge_in_dry_run_mode() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.bounceTrackingProtection.enableDryRunMode", true]], + }); + + await runPurgeTest(false); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_popup.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_popup.js new file mode 100644 index 0000000000..9e6fa8caf1 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_popup.js @@ -0,0 +1,126 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let bounceTrackingProtection; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", true], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); + bounceTrackingProtection = Cc[ + "@mozilla.org/bounce-tracking-protection;1" + ].getService(Ci.nsIBounceTrackingProtection); +}); + +async function runTest(spawnWindowType) { + if (!spawnWindowType || !["newTab", "popup"].includes(spawnWindowType)) { + throw new Error(`Invalid option '${spawnWindowType}' for spawnWindowType`); + } + + Assert.equal( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length, + 0, + "No bounce tracker hosts initially." + ); + Assert.equal( + bounceTrackingProtection.testGetUserActivationHosts({}).length, + 0, + "No user activation hosts initially." + ); + + // Spawn a tab with A, the start of the bounce chain. + await BrowserTestUtils.withNewTab( + getBaseUrl(ORIGIN_A) + "file_start.html", + async browser => { + // The destination site C to navigate to after the bounce. + let finalURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"); + // The middle hop in the bounce chain B that redirects to finalURL C. + let bounceURL = getBounceURL({ + bounceType: "client", + targetURL: finalURL, + setState: "cookie-client", + }); + + // Register a promise for the new popup window. This resolves once the popup + // has opened and the final url (C) has been loaded. + let openPromise; + + if (spawnWindowType == "newTab") { + openPromise = BrowserTestUtils.waitForNewTab(gBrowser, finalURL.href); + } else { + openPromise = BrowserTestUtils.waitForNewWindow({ url: finalURL.href }); + } + + // Navigate through the bounce chain by opening a popup to the bounce URL. + await navigateLinkClick(browser, bounceURL, { + spawnWindow: spawnWindowType, + }); + + let tabOrWindow = await openPromise; + + let tabOrWindowBrowser; + if (spawnWindowType == "newTab") { + tabOrWindowBrowser = tabOrWindow.linkedBrowser; + } else { + tabOrWindowBrowser = tabOrWindow.gBrowser.selectedBrowser; + } + + let promiseRecordBounces = waitForRecordBounces(tabOrWindowBrowser); + + // Navigate again with user gesture which triggers + // BounceTrackingProtection::RecordStatefulBounces. We could rely on the + // timeout (mClientBounceDetectionTimeout) here but that can cause races + // in debug where the load is quite slow. + await navigateLinkClick( + tabOrWindowBrowser, + new URL(getBaseUrl(ORIGIN_C) + "file_start.html") + ); + + info("Wait for bounce trackers to be recorded."); + await promiseRecordBounces; + + // Cleanup popup or tab. + if (spawnWindowType == "newTab") { + await BrowserTestUtils.removeTab(tabOrWindow); + } else { + await BrowserTestUtils.closeWindow(tabOrWindow); + } + } + ); + + // Check that the bounce tracker was detected. + Assert.deepEqual( + bounceTrackingProtection + .testGetBounceTrackerCandidateHosts({}) + .map(entry => entry.siteHost), + [SITE_TRACKER], + "Bounce tracker in popup detected." + ); + + // Cleanup. + bounceTrackingProtection.clearAll(); + await SiteDataTestUtils.clear(); +} + +/** + * Tests that bounce trackers which use popups as the first hop in the bounce + * chain can not bypass detection. + * + * A -> popup -> B -> C + * + * A opens a popup and loads B in it. B is the tracker that performs a + * short-lived redirect and C is the final destination. + */ + +add_task(async function test_popup() { + await runTest("popup"); +}); + +add_task(async function test_new_tab() { + await runTest("newTab"); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js index eedd374197..676ac5fe92 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js @@ -133,8 +133,21 @@ add_task(async function test_purging_skip_content_blocking_allow_list() { "Should only purge example.net. example.org is within the grace period, example.com is allow-listed." ); + // example.net is removed because it is purged, example.com is removed because + // it is allow-listed. + Assert.deepEqual( + bounceTrackingProtection + .testGetBounceTrackerCandidateHosts({}) + .map(entry => entry.siteHost), + ["example.org"], + "Should have removed example.net and example.com from bounce tracker candidate list." + ); + + info("Add example.com as a bounce tracker candidate again."); + bounceTrackingProtection.testAddBounceTrackerCandidate({}, "example.com", 1); + info( - "Remove the allow-list entry for example.com and test that it gets purged now." + "Remove the allow-list entry for example.com and test that it gets purged now." ); await BrowserTestUtils.withNewTab("https://example.com", async browser => { @@ -146,6 +159,16 @@ add_task(async function test_purging_skip_content_blocking_allow_list() { "example.com should have been purged now that it is no longer allow-listed." ); + // example.org is still in the grace period so it neither gets purged nor + // removed from the candidate list. + Assert.deepEqual( + bounceTrackingProtection + .testGetBounceTrackerCandidateHosts({}) + .map(entry => entry.siteHost), + ["example.org"], + "Should have removed example.com from bounce tracker candidate list." + ); + bounceTrackingProtection.clearAll(); }); @@ -166,8 +189,24 @@ add_task( "Should only purge example.net. example.org is within the grace period, example.com is allow-listed via test1.example.com." ); + // example.net is removed because it is purged, example.com is removed because it is allow-listed. + Assert.deepEqual( + bounceTrackingProtection + .testGetBounceTrackerCandidateHosts({}) + .map(entry => entry.siteHost), + ["example.org"], + "Should have removed example.net and example.com from bounce tracker candidate list." + ); + + info("Add example.com as a bounce tracker candidate again."); + bounceTrackingProtection.testAddBounceTrackerCandidate( + {}, + "example.com", + 1 + ); + info( - "Remove the allow-list entry for test1.example.com and test that it gets purged now." + "Remove the allow-list entry for test1.example.com and test that it gets purged now." ); await BrowserTestUtils.withNewTab( @@ -179,7 +218,7 @@ add_task( Assert.deepEqual( await bounceTrackingProtection.testRunPurgeBounceTrackers(), ["example.com"], - "example.com should have been purged now that test1.example.com it is no longer allow-listed." + "example.com should have been purged now that test1.example.com is no longer allow-listed." ); bounceTrackingProtection.clearAll(); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_schemes.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_schemes.js new file mode 100644 index 0000000000..21b72cdc45 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_schemes.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let bounceTrackingProtection; + +add_setup(async function () { + bounceTrackingProtection = Cc[ + "@mozilla.org/bounce-tracking-protection;1" + ].getService(Ci.nsIBounceTrackingProtection); + bounceTrackingProtection.clearAll(); +}); + +async function testInteractWithSite(origin, expectRecorded) { + is( + bounceTrackingProtection.testGetUserActivationHosts({}).length, + 0, + "No user activation hosts initially" + ); + + let baseDomain; + let scheme; + + await BrowserTestUtils.withNewTab(origin, async browser => { + baseDomain = browser.contentPrincipal.baseDomain; + scheme = browser.contentPrincipal.URI.scheme; + + info( + `Trigger a user activation, which should ${ + expectRecorded ? "" : "not " + }be recorded.` + ); + // We intentionally turn off this a11y check, because the following click + // is purposefully sent on an arbitrary web content that is not expected + // to be tested by itself with the browser mochitests, therefore this rule + // check shall be ignored by a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + await BrowserTestUtils.synthesizeMouseAtPoint(50, 50, {}, browser); + AccessibilityUtils.resetEnv(); + }); + if (expectRecorded) { + Assert.deepEqual( + bounceTrackingProtection + .testGetUserActivationHosts({}) + .map(entry => entry.siteHost), + [baseDomain], + `User activation should be recorded for ${scheme} scheme.` + ); + } else { + Assert.deepEqual( + bounceTrackingProtection.testGetUserActivationHosts({}), + [], + `User activation should not be recorded for ${scheme} scheme.` + ); + } + + bounceTrackingProtection.clearAll(); +} + +/** + * Test that we only record user activation for supported schemes. + */ +add_task(async function test_userActivationSchemes() { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await testInteractWithSite("http://example.com", true); + await testInteractWithSite("https://example.com", true); + + await testInteractWithSite("about:blank", false); + await testInteractWithSite("about:robots", false); + await testInteractWithSite( + "file://" + Services.dirsvc.get("TmpD", Ci.nsIFile).path, + false + ); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js index dfbd4d0fc0..cdc21eb788 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js @@ -73,12 +73,18 @@ add_task(async function test_bounce_chain() { await promiseRecordBounces; Assert.deepEqual( - bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).sort(), + bounceTrackingProtection + .testGetBounceTrackerCandidateHosts({}) + .map(entry => entry.siteHost) + .sort(), [SITE_TRACKER_B, SITE_TRACKER].sort(), `Identified all bounce trackers in the redirect chain.` ); Assert.deepEqual( - bounceTrackingProtection.testGetUserActivationHosts({}).sort(), + bounceTrackingProtection + .testGetUserActivationHosts({}) + .map(entry => entry.siteHost) + .sort(), [SITE_A, SITE_B].sort(), "Should only have user activation for sites where we clicked links." ); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_cookies.js index e7fb4521a7..d4940e668d 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_cookies.js @@ -34,6 +34,15 @@ add_task(async function test_bounce_stateful_cookies_client() { }); }); +add_task(async function test_bounce_stateful_cookies_client_sameSiteFrame() { + info("Test client bounce with cookie set in same site frame."); + await runTestBounce({ + bounceType: "client", + setState: "cookie-client", + setStateSameSiteFrame: true, + }); +}); + add_task(async function test_bounce_stateful_cookies_server() { info("Test server bounce with cookie."); await runTestBounce({ @@ -49,15 +58,11 @@ add_task(async function test_bounce_stateful_cookies_server() { }); }); -// Storage tests. - -// TODO: Bug 1848406: Implement stateful bounce detection for localStorage. -add_task(async function test_bounce_stateful_localStorage() { - info("TODO: client bounce with localStorage."); +add_task(async function test_bounce_stateful_cookies_server_sameSiteFrame() { + info("Test client bounce with cookie set in same site frame."); await runTestBounce({ - bounceType: "client", - setState: "localStorage", - expectCandidate: false, - expectPurge: false, + bounceType: "server", + setState: "cookie-server", + setStateSameSiteFrame: true, }); }); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_storage.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_storage.js new file mode 100644 index 0000000000..ff9daabcdb --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_storage.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let bounceTrackingProtection; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", true], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); + bounceTrackingProtection = Cc[ + "@mozilla.org/bounce-tracking-protection;1" + ].getService(Ci.nsIBounceTrackingProtection); +}); + +// Storage tests. + +add_task(async function test_bounce_stateful_localStorage() { + info("Client bounce with localStorage."); + await runTestBounce({ + bounceType: "client", + setState: "localStorage", + }); +}); + +add_task(async function test_bounce_stateful_localStorage_sameSiteFrame() { + info("Client bounce with localStorage set in same site frame."); + await runTestBounce({ + bounceType: "client", + setState: "localStorage", + setStateSameSiteFrame: true, + }); +}); + +add_task(async function test_bounce_stateful_indexedDB() { + info("Client bounce with indexedDB."); + await runTestBounce({ + bounceType: "client", + setState: "indexedDB", + }); +}); + +add_task(async function test_bounce_stateful_indexedDB_sameSiteFrame() { + info("Client bounce with indexedDB populated in same site frame."); + await runTestBounce({ + bounceType: "client", + setState: "indexedDB", + setStateSameSiteFrame: true, + }); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_web_worker.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_web_worker.js new file mode 100644 index 0000000000..1e6672e784 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_web_worker.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let bounceTrackingProtection; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", true], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); + bounceTrackingProtection = Cc[ + "@mozilla.org/bounce-tracking-protection;1" + ].getService(Ci.nsIBounceTrackingProtection); +}); + +add_task(async function test_bounce_stateful_indexedDB() { + info("Client bounce with indexedDB."); + await runTestBounce({ + bounceType: "client", + setState: "indexedDB", + setStateInWebWorker: true, + }); +}); + +// FIXME: (Bug 1889898) This test is skipped because it triggers a shutdown +// hang. +add_task(async function test_bounce_stateful_indexedDB_nestedWorker() { + info("Client bounce with indexedDB access from a nested worker."); + await runTestBounce({ + bounceType: "client", + setState: "indexedDB", + setStateInNestedWebWorker: true, + }); +}).skip(); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_telemetry_purge_duration.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_telemetry_purge_duration.js new file mode 100644 index 0000000000..74b1fdb30d --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_telemetry_purge_duration.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let bounceTrackingProtection; + +async function test_purge_duration(isDryRunMode) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.bounceTrackingProtection.enableDryRunMode", isDryRunMode]], + }); + + is( + Glean.bounceTrackingProtection.purgeDuration.testGetValue(), + null, + "Histogram should not exist initially." + ); + + info("Run server bounce with cookie."); + await runTestBounce({ + bounceType: "server", + setState: "cookie-server", + postBounceCallback: () => { + is( + Glean.bounceTrackingProtection.purgeDuration.testGetValue(), + null, + "Histogram should still be empty after bounce, because we haven't purged yet." + ); + }, + }); + + let events = Glean.bounceTrackingProtection.purgeDuration.testGetValue(); + if (isDryRunMode) { + is(events, null, "Should not collect purge timining in dry mode"); + } else { + is(events.count, 1, "Histogram should contain one value."); + } + + // Cleanup + Services.fog.testResetFOG(); + await SpecialPowers.popPrefEnv(); + bounceTrackingProtection.clearAll(); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", true], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); + bounceTrackingProtection = Cc[ + "@mozilla.org/bounce-tracking-protection;1" + ].getService(Ci.nsIBounceTrackingProtection); + + // Clear telemetry before test. + Services.fog.testResetFOG(); +}); + +add_task(async function test_purge_duration_dry_mode() { + await test_purge_duration(true); +}); + +add_task(async function test_purge_duration_enabled() { + await test_purge_duration(false); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html index 2756555fa5..d7aa117481 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html @@ -1,59 +1,150 @@ <!DOCTYPE html> <html> - <head> - <meta charset="utf-8" /> - <meta http-equiv="X-UA-Compatible" content="IE=edge" /> - <title>Bounce!</title> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - </head> - <body> - <p>Nothing to see here...</p> - <script> - // Wrap the entire block so we can run async code. - (async () => { - let url = new URL(location.href); - - let redirectDelay = url.searchParams.get("redirectDelay"); - if(redirectDelay != null) { - redirectDelay = Number.parseInt(redirectDelay); - } else { - redirectDelay = 50; - } +<head> + <meta charset="utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + <title>Bounce!</title> + <meta name="viewport" content="width=device-width, initial-scale=1" /> +</head> + +<body> + <p id="test-config"></p> + <script> + const SET_STATE_HANDLERS = { + "cookie-client": setCookie, + "localStorage": setLocalStorage, + "indexedDB": setIndexedDB, + }; + + function setCookie(id) { + let cookie = document.cookie; + if (cookie) { + console.info("Received cookie", cookie); + } else { + let newCookie = `id=${id}`; + console.info("Setting new cookie", newCookie); + document.cookie = newCookie; + } + } + + function setLocalStorage(id) { + let entry = localStorage.getItem("id"); + if (entry) { + console.info("Found localStorage entry. id", entry); + } else { + console.info("Setting new localStorage entry. id", id); + localStorage.setItem(id, id); + } + } + + function setIndexedDB() { + return new Promise((resolve, reject) => { + let request = window.indexedDB.open("bounce", 1); + request.onsuccess = () => { + console.info("Opened indexedDB"); + resolve() + }; + request.onerror = (event) => { + console.error("Error opening indexedDB", event); + reject(); + }; + request.onupgradeneeded = (event) => { + console.info("Initializing indexedDB"); + let db = event.target.result; + db.createObjectStore("bounce"); + }; + }); + } + + function setIndexedDBInWorker(nested = false) { + let worker = new Worker("file_web_worker.js"); + let msg = nested ? "setIndexedDBNested" : "setIndexedDB"; + worker.postMessage(msg); + return new Promise((resolve, reject) => { + worker.onmessage = () => { + console.info("IndexedDB set in worker"); + resolve(); + }; + worker.onerror = (event) => { + console.error("Error setting indexedDB in worker", event); + reject(); + }; + }); + } + + /** + * Set a state in a child frame. + */ + function setStateInFrame() { + // Embed self + let iframe = document.createElement("iframe"); + + let src = new URL(location.href); + // Remove search params we don't need for the iframe. + src.searchParams.delete("target"); + src.searchParams.delete("redirectDelay"); + src.searchParams.delete("setStateSameSiteFrame"); + iframe.src = src.href; + + let frameReadyPromise = new Promise((resolve) => { + iframe.addEventListener("load", () => { + iframe.contentWindow.readyPromise.then(resolve); + }); + }); + document.body.appendChild(iframe); + + return frameReadyPromise; + } + + // Wrap the entire block so we can run async code. Store the result in a + // promise so that parent windows can wait for us to be ready. + window.readyPromise = (async () => { + let url = new URL(location.href); + // Display the test config in the body. + document.getElementById("test-config").innerText = JSON.stringify(Object.fromEntries(url.searchParams), null, 2); + + if (url.searchParams.get("setStateSameSiteFrame") === "true") { + // Set state in a child frame. + await setStateInFrame(url); + } else if(url.searchParams.get("setStateInWebWorker") === "true") { + // Set state in a worker. + await setIndexedDBInWorker(); + } else if(url.searchParams.get("setStateInNestedWebWorker") === "true") { + // Set state in a nested worker. + await setIndexedDBInWorker(true); + } else { + // Set a state in this window. let setState = url.searchParams.get("setState"); if (setState) { let id = Math.random().toString(); - if (setState == "cookie-client") { - let cookie = document.cookie; - - if (cookie) { - console.info("Received cookie", cookie); - } else { - let newCookie = `id=${id}`; - console.info("Setting new cookie", newCookie); - document.cookie = newCookie; - } - } else if (setState == "localStorage") { - let entry = localStorage.getItem("id"); - - if (entry) { - console.info("Found localStorage entry. id", entry); - } else { - console.info("Setting new localStorage entry. id", id); - localStorage.setItem(id, id); - } + let handler = SET_STATE_HANDLERS[setState]; + if (!handler) { + throw new Error("Unknown state handler: " + setState); } + await handler(id); } + } + + // Redirect to the target URL after a delay. + // If no target is specified, do nothing. + let redirectDelay = url.searchParams.get("redirectDelay"); + if (redirectDelay != null) { + redirectDelay = Number.parseInt(redirectDelay); + } else { + redirectDelay = 50; + } + + let target = url.searchParams.get("target"); + if (target) { + console.info("Redirecting to", target); + setTimeout(() => { + location.href = target; + }, redirectDelay); + } + })(); + </script> +</body> - let target = url.searchParams.get("target"); - if (target) { - console.info("redirecting to", target); - setTimeout(() => { - location.href = target; - }, redirectDelay); - } - })(); - </script> - </body> </html> diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_web_worker.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_web_worker.js new file mode 100644 index 0000000000..78fdf2a149 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_web_worker.js @@ -0,0 +1,40 @@ +// A web worker which can set indexedDB. + +function setIndexedDB() { + return new Promise((resolve, reject) => { + let request = self.indexedDB.open("bounce", 1); + request.onsuccess = () => { + console.info("Opened indexedDB"); + resolve(); + }; + request.onerror = event => { + console.error("Error opening indexedDB", event); + reject(); + }; + request.onupgradeneeded = event => { + console.info("Initializing indexedDB"); + let db = event.target.result; + db.createObjectStore("bounce"); + }; + }); +} + +self.onmessage = function (event) { + console.info("Web worker received message", event.data); + + if (event.data === "setIndexedDB") { + setIndexedDB().then(() => { + self.postMessage("indexedDBSet"); + }); + } else if (event.data === "setIndexedDBNested") { + console.info("set state nested"); + // Rather than setting indexedDB in this worker spawn a nested worker to set + // indexedDB. + let nestedWorker = new Worker("file_web_worker.js"); + nestedWorker.postMessage("setIndexedDB"); + nestedWorker.onmessage = () => { + console.info("IndexedDB set in nested worker"); + self.postMessage("indexedDBSet"); + }; + } +}; diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js index f5857b6919..71d9acedc6 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js @@ -3,6 +3,17 @@ "use strict"; +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "bounceTrackingProtection", + "@mozilla.org/bounce-tracking-protection;1", + "nsIBounceTrackingProtection" +); + const SITE_A = "example.com"; const ORIGIN_A = `https://${SITE_A}`; @@ -25,13 +36,6 @@ const OBSERVER_MSG_RECORD_BOUNCES_FINISHED = "test-record-bounces-finished"; const ROOT_DIR = getRootDirectory(gTestPath); -XPCOMUtils.defineLazyServiceGetter( - this, - "bounceTrackingProtection", - "@mozilla.org/bounce-tracking-protection;1", - "nsIBounceTrackingProtection" -); - /** * Get the base url for the current test directory using the given origin. * @param {string} origin - Origin to use in URL. @@ -48,8 +52,14 @@ function getBaseUrl(origin) { * the bounce. * @param {string} [options.bounceOrigin] - The origin of the bounce URL. * @param {string} [options.targetURL] - URL to redirect to after the bounce. - * @param {("cookie"|null)} [options.setState] - What type of state should be set during - * the bounce. No state by default. + * @param {('cookie-server'|'cookie-client'|'localStorage')} [options.setState] + * Type of state to set during the redirect. Defaults to non stateful redirect. + * @param {boolean} [options.setStateSameSiteFrame=false] - Whether to set the + * state in a sub frame that is same site to the top window. + * @param {boolean} [options.setStateInWebWorker=false] - Whether to set the + * state in a web worker. This only supports setState == "indexedDB". + * @param {boolean} [options.setStateInWebWorker=false] - Whether to set the + * state in a nested web worker. Otherwise the same as setStateInWebWorker. * @param {number} [options.statusCode] - HTTP status code to use for server * side redirect. Only applies to bounceType == "server". * @param {number} [options.redirectDelayMS] - How long to wait before @@ -62,6 +72,9 @@ function getBounceURL({ bounceOrigin = ORIGIN_TRACKER, targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"), setState = null, + setStateSameSiteFrame = false, + setStateInWebWorker = false, + setStateInNestedWebWorker = false, statusCode = 302, redirectDelayMS = 50, }) { @@ -79,6 +92,25 @@ function getBounceURL({ if (setState) { searchParams.set("setState", setState); } + if (setStateSameSiteFrame) { + searchParams.set("setStateSameSiteFrame", setStateSameSiteFrame); + } + if (setStateInWebWorker) { + if (setState != "indexedDB") { + throw new Error( + "setStateInWebWorker only supports setState == 'indexedDB'" + ); + } + searchParams.set("setStateInWebWorker", setStateInWebWorker); + } + if (setStateInNestedWebWorker) { + if (setState != "indexedDB") { + throw new Error( + "setStateInNestedWebWorker only supports setState == 'indexedDB'" + ); + } + searchParams.set("setStateInNestedWebWorker", setStateInNestedWebWorker); + } if (bounceType == "server") { searchParams.set("statusCode", statusCode); @@ -94,23 +126,58 @@ function getBounceURL({ * click on it. * @param {MozBrowser} browser - Browser to insert the link in. * @param {URL} targetURL - Destination for navigation. + * @param {Object} options - Additional options. + * @param {string} [options.spawnWindow] - If set to "newTab" or "popup" the + * link will be opened in a new tab or popup window respectively. If unset the + * link is opened in the given browser. * @returns {Promise} Resolves once the click is done. Does not wait for * navigation or load. */ -async function navigateLinkClick(browser, targetURL) { - await SpecialPowers.spawn(browser, [targetURL.href], targetURL => { - let link = content.document.createElement("a"); - - link.href = targetURL; - link.textContent = targetURL; - // The link needs display: block, otherwise synthesizeMouseAtCenter doesn't - // hit it. - link.style.display = "block"; - - content.document.body.appendChild(link); - }); +async function navigateLinkClick( + browser, + targetURL, + { spawnWindow = null } = {} +) { + if (spawnWindow && !["newTab", "popup"].includes(spawnWindow)) { + throw new Error(`Invalid option '${spawnWindow}' for spawnWindow`); + } - await BrowserTestUtils.synthesizeMouseAtCenter("a[href]", {}, browser); + await SpecialPowers.spawn( + browser, + [targetURL.href, spawnWindow], + async (targetURL, spawnWindow) => { + let link = content.document.createElement("a"); + link.id = "link"; + link.textContent = "Click Me"; + link.style.display = "block"; + link.style.fontSize = "40px"; + + // For opening a popup we attach an event listener to trigger via click. + if (spawnWindow) { + link.href = "#"; + link.addEventListener("click", event => { + event.preventDefault(); + if (spawnWindow == "newTab") { + // Open a new tab. + content.window.open(targetURL, "bounce"); + } else { + // Open a popup window. + content.window.open(targetURL, "bounce", "height=200,width=200"); + } + }); + } else { + // For regular navigation add href and click. + link.href = targetURL; + } + + content.document.body.appendChild(link); + + // TODO: Bug 1892091: Use EventUtils.synthesizeMouse instead for a real click. + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + content.document.userInteractionForTesting(); + link.click(); + } + ); } /** @@ -141,25 +208,38 @@ async function waitForRecordBounces(browser) { * or server side redirect. * @param {('cookie-server'|'cookie-client'|'localStorage')} [options.setState] * Type of state to set during the redirect. Defaults to non stateful redirect. - * @param {boolean} [options.expectCandidate=true] - Expect the redirecting site to be - * identified as a bounce tracker (candidate). - * @param {boolean} [options.expectPurge=true] - Expect the redirecting site to have - * its storage purged. + * @param {boolean} [options.setStateSameSiteFrame=false] - Whether to set the + * state in a sub frame that is same site to the top window. + * @param {boolean} [options.setStateInWebWorker=false] - Whether to set the + * state in a web worker. This only supports setState == "indexedDB". + * @param {boolean} [options.setStateInWebWorker=false] - Whether to set the + * state in a nested web worker. Otherwise the same as setStateInWebWorker. + * @param {boolean} [options.expectCandidate=true] - Expect the redirecting site + * to be identified as a bounce tracker (candidate). + * @param {boolean} [options.expectPurge=true] - Expect the redirecting site to + * have its storage purged. * @param {OriginAttributes} [options.originAttributes={}] - Origin attributes * to use for the test. This determines whether the test is run in normal - * browsing, a private window or a container tab. By default the test is run - * in normal browsing. - * @param {function} [options.postBounceCallback] - Optional function to run after the - * bounce has completed. + * browsing, a private window or a container tab. By default the test is run in + * normal browsing. + * @param {function} [options.postBounceCallback] - Optional function to run + * after the bounce has completed. + * @param {boolean} [options.skipSiteDataCleanup=false] - Skip the cleanup of + * site data after the test. When this is enabled the caller is responsible for + * cleaning up site data. */ async function runTestBounce(options = {}) { let { bounceType, setState = null, + setStateSameSiteFrame = false, + setStateInWebWorker = false, + setStateInNestedWebWorker = false, expectCandidate = true, expectPurge = true, originAttributes = {}, postBounceCallback = () => {}, + skipSiteDataCleanup = false, } = options; info(`runTestBounce ${JSON.stringify(options)}`); @@ -191,28 +271,42 @@ async function runTestBounce(options = {}) { win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); } - let tab = win.gBrowser.addTab(getBaseUrl(ORIGIN_A) + "file_start.html", { + let initialURL = getBaseUrl(ORIGIN_A) + "file_start.html"; + let tab = win.gBrowser.addTab(initialURL, { triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), userContextId, }); win.gBrowser.selectedTab = tab; let browser = tab.linkedBrowser; - await BrowserTestUtils.browserLoaded(browser); + await BrowserTestUtils.browserLoaded(browser, true, initialURL); let promiseRecordBounces = waitForRecordBounces(browser); // The final destination after the bounce. let targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"); + // Wait for the final site to be loaded which complete the BounceTrackingRecord. + let targetURLLoadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + targetURL + ); + // Navigate through the bounce chain. await navigateLinkClick( browser, - getBounceURL({ bounceType, targetURL, setState }) + getBounceURL({ + bounceType, + targetURL, + setState, + setStateSameSiteFrame, + setStateInWebWorker, + setStateInNestedWebWorker, + }) ); - // Wait for the final site to be loaded which complete the BounceTrackingRecord. - await BrowserTestUtils.browserLoaded(browser, false, targetURL); + await targetURLLoadedPromise; // Navigate again with user gesture which triggers // BounceTrackingProtection::RecordStatefulBounces. We could rely on the @@ -226,9 +320,9 @@ async function runTestBounce(options = {}) { await promiseRecordBounces; Assert.deepEqual( - bounceTrackingProtection.testGetBounceTrackerCandidateHosts( - originAttributes - ), + bounceTrackingProtection + .testGetBounceTrackerCandidateHosts(originAttributes) + .map(entry => entry.siteHost), expectCandidate ? [SITE_TRACKER] : [], `Should ${ expectCandidate ? "" : "not " @@ -237,6 +331,7 @@ async function runTestBounce(options = {}) { Assert.deepEqual( bounceTrackingProtection .testGetUserActivationHosts(originAttributes) + .map(entry => entry.siteHost) .sort(), [SITE_A, SITE_B].sort(), "Should only have user activation for sites where we clicked links." @@ -272,4 +367,7 @@ async function runTestBounce(options = {}) { ); } bounceTrackingProtection.clearAll(); + if (!skipSiteDataCleanup) { + await SiteDataTestUtils.clear(); + } } diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py index afc3239839..3b0fe351e2 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py @@ -48,7 +48,7 @@ class BounceTrackingStoragePersistenceTestCase(MarionetteTestCase): let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( Ci.nsIBounceTrackingProtection ); - return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).sort(); + return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).map(entry => entry.siteHost).sort(); """, ) self.assertEqual( @@ -64,7 +64,7 @@ class BounceTrackingStoragePersistenceTestCase(MarionetteTestCase): let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( Ci.nsIBounceTrackingProtection ); - return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ userContextId: 3 }).sort(); + return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ userContextId: 3 }).map(entry => entry.siteHost).sort(); """, ) self.assertEqual( @@ -109,7 +109,7 @@ class BounceTrackingStoragePersistenceTestCase(MarionetteTestCase): let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( Ci.nsIBounceTrackingProtection ); - return bounceTrackingProtection.testGetUserActivationHosts({}).sort(); + return bounceTrackingProtection.testGetUserActivationHosts({}).map(entry => entry.siteHost).sort(); """, ) self.assertEqual( diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_clearExpiredUserActivation.js b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_clearExpiredUserActivation.js index 28a1350b3e..a52fb7fd46 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_clearExpiredUserActivation.js +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_clearExpiredUserActivation.js @@ -69,13 +69,19 @@ add_task(async function test() { // Assert that expired user activations have been cleared. Assert.deepEqual( - btp.testGetUserActivationHosts({}).sort(), + btp + .testGetUserActivationHosts({}) + .map(entry => entry.siteHost) + .sort(), ["not-expired1.com", "not-expired2.com"], "Expired user activation flags have been cleared for normal browsing." ); Assert.deepEqual( - btp.testGetUserActivationHosts({ privateBrowsingId: 1 }).sort(), + btp + .testGetUserActivationHosts({ privateBrowsingId: 1 }) + .map(entry => entry.siteHost) + .sort(), ["pbm-not-expired.com"], "Expired user activation flags have been cleared for private browsing." ); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_importUserActivationPermissions.js b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_importUserActivationPermissions.js new file mode 100644 index 0000000000..5150d074c2 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_importUserActivationPermissions.js @@ -0,0 +1,153 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +const DOMAIN_A = "example.com"; +const SUB_DOMAIN_A = "sub." + DOMAIN_A; +const DOMAIN_B = "example.org"; +const DOMAIN_C = "example.net"; + +const ORIGIN_A = "https://" + DOMAIN_A; +const ORIGIN_SUB_A = "https://" + SUB_DOMAIN_A; +const ORIGIN_B = "https://" + DOMAIN_B; +const ORIGIN_C = "https://" + DOMAIN_C; +const ORIGIN_NON_HTTP = "file:///foo/bar.html"; + +const OA_PBM = { privateBrowsingId: 1 }; +const PRINCIPAL_C_PBM = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(ORIGIN_C), + OA_PBM +); + +let btp; +let userActivationLifetimeSec = Services.prefs.getIntPref( + "privacy.bounceTrackingProtection.bounceTrackingActivationLifetimeSec" +); + +function cleanup() { + btp.clearAll(); + Services.perms.removeAll(); + Services.prefs.setBoolPref( + "privacy.bounceTrackingProtection.hasMigratedUserActivationData", + false + ); +} + +add_setup(function () { + // Need a profile to data clearing calls. + do_get_profile(); + + btp = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + + // Clean initial state. + cleanup(); +}); + +add_task(async function test_user_activation_perm_migration() { + // Assert initial test state. + Assert.deepEqual( + btp.testGetUserActivationHosts({}), + [], + "No user activation hosts initially." + ); + Assert.equal( + Services.perms.getAllByTypes(["storageAccessAPI"]).length, + 0, + "No user activation permissions initially." + ); + + info("Add test user activation permissions."); + + let now = Date.now(); + + // Non-expired permissions. + PermissionTestUtils.addWithModificationTime( + ORIGIN_A, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + now + ); + PermissionTestUtils.addWithModificationTime( + ORIGIN_C, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + now - 1000 + ); + + // A non expired permission for a subdomain of DOMAIN_A that has an older modification time. + PermissionTestUtils.addWithModificationTime( + ORIGIN_SUB_A, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + now - 500 + ); + + // An expired permission. + PermissionTestUtils.addWithModificationTime( + ORIGIN_B, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + now - userActivationLifetimeSec * 1.2 * 1000 + ); + + // A non-HTTP permission. + PermissionTestUtils.addWithModificationTime( + ORIGIN_NON_HTTP, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + now + ); + + // A permission for PBM. Ideally we'd test a more persistent permission type + // here with custom oa, but permission seperation by userContextId isn't + // enabled yet (Bug 1641584). + PermissionTestUtils.addWithModificationTime( + PRINCIPAL_C_PBM, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + now + ); + + info("Trigger migration."); + btp.testMaybeMigrateUserInteractionPermissions(); + + Assert.deepEqual( + btp + .testGetUserActivationHosts({}) + .map(entry => entry.siteHost) + .sort(), + [DOMAIN_A, DOMAIN_C].sort(), + "Should have imported the correct user activation flags." + ); + Assert.deepEqual( + btp.testGetUserActivationHosts(OA_PBM).map(entry => entry.siteHost), + [DOMAIN_C], + "Should have imported the correct user activation flags for PBM." + ); + + info("Reset the BTP user activation store"); + btp.clearAll(); + + info("Trigger migration again."); + btp.testMaybeMigrateUserInteractionPermissions(); + + Assert.deepEqual( + btp.testGetUserActivationHosts({}), + [], + "Should not have imported the user activation flags again." + ); + Assert.deepEqual( + btp.testGetUserActivationHosts(OA_PBM), + [], + "Should not have imported the user activation flags again for PBM." + ); + + cleanup(); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js index 5ede57a08b..2fbd8a0a02 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js @@ -6,6 +6,9 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ const { SiteDataTestUtils } = ChromeUtils.importESModule( "resource://testing-common/SiteDataTestUtils.sys.mjs" ); +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); let btp; let bounceTrackingGracePeriodSec; @@ -138,6 +141,14 @@ add_task(async function test_purge() { message: "Should purge after grace period.", shouldPurge: true, }, + // Don't purge if the site is allowlisted. + "example2.net": { + bounceTime: timestampOutsideGracePeriodFiveSeconds, + userActivationTime: null, + isAllowListed: true, + message: "Should not purge after grace period if allowlisted.", + shouldPurge: false, + }, // Also ensure that clear data calls with IP sites succeed. "1.2.3.4": { bounceTime: timestampOutsideGracePeriodThreeDays, @@ -191,16 +202,30 @@ add_task(async function test_purge() { let expectedBounceTrackerHosts = []; let expectedUserActivationHosts = []; + let allowListedHosts = []; let expiredUserActivationHosts = []; let expectedPurgedHosts = []; // This would normally happen over time while browsing. let initPromises = Object.entries(TEST_TRACKERS).map( - async ([siteHost, { bounceTime, userActivationTime, shouldPurge }]) => { + async ([ + siteHost, + { bounceTime, userActivationTime, isAllowListed, shouldPurge }, + ]) => { // Add site state so we can later assert it has been purged. await addStateForHost(siteHost); + // Add allowlist entry if needed. + if (isAllowListed) { + PermissionTestUtils.add( + `https://${siteHost}`, + "trackingprotection", + Services.perms.ALLOW_ACTION + ); + allowListedHosts.push(siteHost); + } + if (bounceTime != null) { if (userActivationTime != null) { throw new Error( @@ -210,7 +235,7 @@ add_task(async function test_purge() { expectedBounceTrackerHosts.push(siteHost); - // Convert bounceTime timestamp to nanoseconds (PRTime). + // Convert bounceTime timestamp to microseconds (PRTime). info( `Adding bounce. siteHost: ${siteHost}, bounceTime: ${bounceTime} ms` ); @@ -232,7 +257,7 @@ add_task(async function test_purge() { expiredUserActivationHosts.push(siteHost); } - // Convert userActivationTime timestamp to nanoseconds (PRTime). + // Convert userActivationTime timestamp to microseconds (PRTime). info( `Adding user interaction. siteHost: ${siteHost}, userActivationTime: ${userActivationTime} ms` ); @@ -250,12 +275,18 @@ add_task(async function test_purge() { "Check that bounce and user activation data has been correctly recorded." ); Assert.deepEqual( - btp.testGetBounceTrackerCandidateHosts({}).sort(), + btp + .testGetBounceTrackerCandidateHosts({}) + .map(entry => entry.siteHost) + .sort(), expectedBounceTrackerHosts.sort(), "Has added bounce tracker hosts." ); Assert.deepEqual( - btp.testGetUserActivationHosts({}).sort(), + btp + .testGetUserActivationHosts({}) + .map(entry => entry.siteHost) + .sort(), expectedUserActivationHosts.sort(), "Has added user activation hosts." ); @@ -269,17 +300,29 @@ add_task(async function test_purge() { "Should have purged all expected hosts." ); + // After the purge only the bounce trackers that have not been purged should + // remain in the candidate map. Additionally the allowlisted hosts should be + // removed from the candidate map. let expectedBounceTrackerHostsAfterPurge = expectedBounceTrackerHosts - .filter(host => !expectedPurgedHosts.includes(host)) + .filter( + host => + !expectedPurgedHosts.includes(host) && !allowListedHosts.includes(host) + ) .sort(); Assert.deepEqual( - btp.testGetBounceTrackerCandidateHosts({}).sort(), + btp + .testGetBounceTrackerCandidateHosts({}) + .map(entry => entry.siteHost) + .sort(), expectedBounceTrackerHostsAfterPurge.sort(), "After purge the bounce tracker candidate host set should be updated correctly." ); Assert.deepEqual( - btp.testGetUserActivationHosts({}).sort(), + btp + .testGetUserActivationHosts({}) + .map(entry => entry.siteHost) + .sort(), expiredUserActivationHosts.sort(), "After purge any expired user activation records should have been removed" ); @@ -302,6 +345,7 @@ add_task(async function test_purge() { btp.clearAll(); assertEmpty(); - info("Clean up site data."); + info("Clean up site data and permissions."); await SiteDataTestUtils.clear(); + Services.perms.removeAll(); }); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml index c3aeee502f..8195ea7224 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml @@ -3,8 +3,11 @@ prefs = [ "privacy.bounceTrackingProtection.enabled=true", "privacy.bounceTrackingProtection.enableTestMode=true", "privacy.bounceTrackingProtection.bounceTrackingPurgeTimerPeriodSec=0", + "privacy.bounceTrackingProtection.enableDryRunMode=false", ] ["test_bouncetracking_clearExpiredUserActivation.js"] +["test_bouncetracking_importUserActivationPermissions.js"] + ["test_bouncetracking_purge.js"] |